Search code examples
iossprite-kitwatchkitskspritenode

WatchKit Move a SimpleSpriteNode in SpriteKit Game


I would like to know if anyone has a way to move a SKSpriteNode in SpriteKit Watch Game Using WKCrownDelegate. either in Y direction or X direction

Hope this Helps Others that are Starting with WatchKit.

This is my GameElement.swift:

extension GameScene {

  func addPlayer() {
    player = SKSpriteNode(imageNamed: "Spaceship")
    player.setScale(0.15)
    player.position = CGPoint(x: 5, y: -60)
    player.name = “ONE”
    player.physicsBody?.isDynamic = false
    player.physicsBody = SKPhysicsBody(rectangleOf: player.size)


    player2 = SKSpriteNode(imageNamed: "Spaceship")
    player2.setScale(0.15)
    player2.position = CGPoint(x: 5, y: -60)
    player2.name = “ONE”
    player2.physicsBody?.isDynamic = false
    player2.physicsBody = SKPhysicsBody(rectangleOf: player2.size)

    addChild(player)
    addChild(player2)

    playerPosition = player.position
  }
}

This is my GameScene.swift:

class GameScene:  SKScene, SKPhysicsContactDelegate, WKCrownDelegate {

  var watchParticles:SKEmitterNode!

  var player:SKSpriteNode!
  var player2:SKSpriteNode!

  var playerPosition:CGPoint!

  override func sceneDidLoad() {

    self.scaleMode = SKSceneScaleMode.aspectFill

    watchParticles = SKEmitterNode(fileNamed: "watchParticles")
    addChild(watchParticles)


    self.physicsWorld.gravity = CGVector(dx: 0 , dy: 0)
    physicsWorld.contactDelegate = self
    addPlayer()
  }

  func moveSprite(player : SKSpriteNode,moveDirection: String){

    switch moveDirection {
    case "UP":
      print("UP")
      player.childNode(withName: "ONE")?.physicsBody?.applyImpulse(CGVector(dx: 60, dy: 0))
    case "DOWN":
      print("DOWN")
      player.childNode(withName: "ONE")?.physicsBody?.applyImpulse(CGVector(dx: -60, dy: 0))
    case "STOP":
      print("STOPPED")
      player.childNode(withName: "ONE")?.physicsBody?.velocity = CGVector(dx: 0, dy: 0)

    default:
      break
    }
  }
}

This is My InterfaceController.swift:

class InterfaceController: WKInterfaceController, WKCrownDelegate {

  @IBOutlet var skInterface: WKInterfaceSKScene!
  private var moveDirection = ""
  private var game = GameScene()
  private var player = GameScene()

  override func awake(withContext context: Any?) {
    super.awake(withContext: context)
    crownSequencer.delegate = self
    crownSequencer.focus()

    // Configure interface objects here.

    // Load the SKScene from 'GameScene.sks'
    if let scene = GameScene(fileNamed: "GameScene") {

      // Set the scale mode to scale to fit the window
      scene.scaleMode = .aspectFill

      // Present the scene
      self.skInterface.presentScene(scene)
      crownSequencer.delegate = self
      crownSequencer.focus()


      // Use a value that will maintain a consistent frame rate
      self.skInterface.preferredFramesPerSecond = 30
    }
  }

  func crownDidRotate(_ crownSequencer: WKCrownSequencer?, rotationalDelta: Double) {
    if rotationalDelta > 0{
      moveDirection = "UP"
      game.moveSprite(player:  player.player, moveDirection: moveDirection)

    }else if rotationalDelta < 0{
      moveDirection = "DOWN"
      game.moveSprite(player:  player.player, moveDirection: moveDirection)
    }
  }

  func crownDidBecomeIdle(_ crownSequencer: WKCrownSequencer?) {
    moveDirection = "STOP"
    game.moveSprite(player:  player.player, moveDirection: moveDirection)

  }

  override func willActivate() {
    // This method is called when watch view controller is about to be visible to user
    super.willActivate()
  }

  override func didDeactivate() {
    // This method is called when watch view controller is no longer visible
    super.didDeactivate()
  }
}

Solution

  • Welcome to SO!

    Ok, there is a lot to unpack so get some popcorn... I think that you are on the right track here, mostly you need to check for nil when you get errors.

    First, this is wrong in your interface controller, and the part that concerned me. Here, you are just instantiating new GameScene instances, which are completely separate from the gameScene instance created by your interface controller a few lines down. Then, you were sending the crown delegate functions to these totally empty gameScenes.:

    private var game = GameScene()   // You are referencing nothing here, just creating a new gamescene.
    private var player = GameScene() // I don't think that player is supposed to be a gamescene!
    

    I fixed it by doing assigning the actual gameScene you want to use to the properties (so they can be used by the crown delegate).

    private var game: GameScene!
    lazy private var player: SKSpriteNode = self.game.player
    
    override func awake(withContext context: Any?) {
      // ... Stuff...
      if let scene = GameScene(fileNamed: "GameScene") {
        game = scene
    

    This was also changed to represent the new code in your crown delegates:

    game.moveSprite(player: player, moveDirection: moveDirection)
    

    In addPlayer you were doing this:

    player.physicsBody?.isDynamic = true // This needs to go AFTER you init your pb.
    player.physicsBody = SKPhysicsBody(rectangleOf: player.size)
    

    ...and I fixed it by swapping the lines.

    Personally I like to do the following, to help ensure no small mistakes are made:

    let pb = SKPhysicsBody(...)
    pb.isDynamic = true
    player.physicsBody = pb
    

    moveSprite had a bunch of issues with it, so I'm not going to enumerate them as I did above. Check out what I did then ask me if you have any questions. Basically, you were doomed with this func from the start, because you were calling this method from the interface controller with the whacked out player values that were all nil.

    Also, the .applyImpulse was giving me pretty bad controls, so I changed it to a plain adjustment of .position. There is still a small issue with coasting before the player stops, but that can be handled in another question :) (note, I only tested this on simulator.. may not be an issue on-device).

    Also also, I hate errors caused by spelling mistakes in strings, so I converted this to an enum for you.

    func moveSprite(player : SKSpriteNode, moveDirection: Direction) {
    
      // This will give us an equal amount of pixels to move across the watch devices:
      // Adjust this number for shorter / longer movements:
      let percentageOfScreenToMovePerRotation = CGFloat(1) // One percent
      let modifier = percentageOfScreenToMovePerRotation / 100
      let amountToMove = self.frame.maxX * modifier
    
      switch moveDirection {
    
      case .UP:
        player.position.x += amountToMove
      case .DOWN:
        player.position.x -= amountToMove
      case .STOP:
        break
      }
    }
    

    The real moral of the story here is to check for nil. If you just use someOptional?.someMethod() all the time, then you likely will not be able to easily determine whether or not someMethod() is actually being called or not.. thus, you don't know if the problem is with the calling logic, the method, or with the object not existing, and etc.

    Force unwrapping is frowned upon in production code, but IMO it is extremely valuable when first starting out--because it helps you to quickly identify errors.

    Later on, you can start using things like if let and guard to help check for nil without crashing your programs, but that adds more clutter and complexity to your code when you are trying to just learn the basics of a new API and language.

    And as a final tip, try to not use hard-coded strings whenever possible: put them into an enum or a constant as I have in your code:

    // Because I hate string spelling erros, and you probably do too!
    enum Direction {
      case UP, DOWN, STOP
    }
    
    // Because I hate errors related to spelling in strings:
    let names = (ONE: "ONE", TWO: "TWO")
    


    Here are the two files in their entirety.. note, I had to comment out a few things to get it to work in my project:


    GameScene:

    // Because I hate string spelling erros, and you probably do too!
    enum Direction {
      case UP, DOWN, STOP
    }
    
    class GameScene:  SKScene, SKPhysicsContactDelegate, WKCrownDelegate {
    
      var watchParticles:SKEmitterNode!
    
      var player: SKSpriteNode!
      var player2: SKSpriteNode!
    
      var playerPosition:CGPoint!
    
      // Because I hate errors related to spelling in strings:
      let names = (ONE: "ONE", TWO: "TWO")
    
      func addPlayer() {
        player = SKSpriteNode(color: .blue, size: CGSize(width: 50, height: 50))
        // player = SKSpriteNode(imageNamed: "Spaceship")
        //  player.setScale(0.15)
        player.position = CGPoint(x: 5, y: -60)
        player.name = names.ONE
        player.physicsBody = SKPhysicsBody(rectangleOf: player.size)
        player.physicsBody!.isDynamic = true // This was placed *before* pb initialzier (thus never got called)
    
        player2 = SKSpriteNode(color: .yellow, size: CGSize(width: 50, height: 50))
        // player2 = SKSpriteNode(imageNamed: "Spaceship")
        //  player2.setScale(0.15)
        player2.position = CGPoint(x: 5, y: -60)
        player2.name = names.TWO
        player2.physicsBody = SKPhysicsBody(rectangleOf: player2.size)
        player2.physicsBody!.isDynamic = false // This was placed *before* pb initialzier (thus never got called)
    
        addChild(player)
        addChild(player2)
    
        playerPosition = player.position
      }
    
      override func sceneDidLoad() {
    
        self.scaleMode = SKSceneScaleMode.aspectFill
    
        //watchParticles = SKEmitterNode(fileNamed: "watchParticles")
        //addChild(watchParticles)
    
        self.physicsWorld.gravity = CGVector.zero
        physicsWorld.contactDelegate = self
        addPlayer()
      }
    
        func moveSprite(player : SKSpriteNode, moveDirection: Direction) {
    
          // This will give us an equal amount of pixels to move across the watch devices:
          // Adjust this number for shorter / longer movements:
          let percentageOfScreenToMovePerRotation = CGFloat(1) // One percent
          let modifier = percentageOfScreenToMovePerRotation / 100
          let amountToMove = self.frame.maxX * modifier
    
          switch moveDirection {
    
          case .UP:
            player.position.x += amountToMove
          case .DOWN:
            player.position.x -= amountToMove
          case .STOP:
            break
          }
        }
    }
    

    InterfaceController:

    class InterfaceController: WKInterfaceController, WKCrownDelegate {
    
      @IBOutlet var skInterface: WKInterfaceSKScene!
      private var moveDirection = Direction.STOP
    
      private var game: GameScene!
      lazy private var player: SKSpriteNode = self.game.player
    
      override func awake(withContext context: Any?) {
        super.awake(withContext: context)
        crownSequencer.delegate = self
        crownSequencer.focus()
    
        if let scene = GameScene(fileNamed: "GameScene") {
    
          game = scene // VERY IMPORTANT!
    
          // Set the scale mode to scale to fit the window
          scene.scaleMode = .aspectFill
    
          // Present the scene
          self.skInterface.presentScene(scene)
          crownSequencer.delegate = self
          crownSequencer.focus()
    
          // Use a value that will maintain a consistent frame rate
          self.skInterface.preferredFramesPerSecond = 30
        }
        else {
          fatalError("scene not found")
        }
      }
    
      func crownDidRotate(_ crownSequencer: WKCrownSequencer?, rotationalDelta: Double) {
        if rotationalDelta > 0{
          moveDirection = .UP
          game.moveSprite(player: player, moveDirection: moveDirection)
    
        } else if rotationalDelta < 0{
          moveDirection = .DOWN
          game.moveSprite(player:  player, moveDirection: moveDirection)
        }
      }
    
      func crownDidBecomeIdle(_ crownSequencer: WKCrownSequencer?) {
        moveDirection = .STOP
        game.moveSprite(player: player, moveDirection: moveDirection)
      }
    
      override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
      }
    
      override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
      }
    }