uz7 uz7 - 20 days ago 7
iOS Question

linking GameViewController.swift to GameScene.swift

I have created a UI elements on main.storyboard which i require to be hidden until the game is over and the once the player tap the screen to dismiss. Main.storyboard is linked to GameViewController therefor all my IBOutlets and IBActions are in there and all my game code is in GameScene. How can i link the view controller to the scene for that the popup image and buttons only appear when it is game over. Would greatly appreciate some help, I have been stuck on this for quite some time now.

GameScene.Swift

import SpriteKit
import GameplayKit

class GameScene: SKScene, SKPhysicsContactDelegate {

var shareButton = SKSpriteNode()

var santa = SKSpriteNode()

var bg = SKSpriteNode()

var scoreLabel = SKLabelNode()

var tapToPlayLabel = SKLabelNode()

var score = 0

var gameOverScreen = SKSpriteNode()

var timer = Timer()

enum ColliderType: UInt32 {

case santa = 1
case Object = 2
case Gap = 4

}

enum ButtonName: String {

case play
case share

}

var gameOver = false

func makeBlocks() {

let moveBlocks = SKAction.move(by: CGVector(dx: -2 * self.frame.width, dy: 0), duration: TimeInterval(self.frame.width / 100))

let gapHeight = santa.size.height * 4

let movementAmount = arc4random() % UInt32(self.frame.height / 2)

let blockOffset = CGFloat(movementAmount) - self.frame.height / 4

let blockTexture = SKTexture(imageNamed: "block1.png")

let block1 = SKSpriteNode(texture: blockTexture)

block1.position = CGPoint(x: self.frame.midX + self.frame.width, y: self.frame.midY + blockTexture.size().height / 2 + gapHeight / 2 + blockOffset)

block1.run(moveBlocks)

block1.physicsBody = SKPhysicsBody(rectangleOf: blockTexture.size())
block1.physicsBody!.isDynamic = false

block1.physicsBody!.contactTestBitMask = ColliderType.Object.rawValue
block1.physicsBody!.categoryBitMask = ColliderType.Object.rawValue
block1.physicsBody!.collisionBitMask = ColliderType.Object.rawValue

block1.zPosition = -2

self.addChild(block1)

let block2Texture = SKTexture(imageNamed: "block2.png")

let block2 = SKSpriteNode(texture: block2Texture)

block2.position = CGPoint(x: self.frame.midX + self.frame.width, y: self.frame.midY - block2Texture.size().height / 2 - gapHeight / 2 + blockOffset)

block2.run(moveBlocks)

block2.physicsBody = SKPhysicsBody(rectangleOf: blockTexture.size())
block2.physicsBody!.isDynamic = false

block2.physicsBody!.contactTestBitMask = ColliderType.Object.rawValue
block2.physicsBody!.categoryBitMask = ColliderType.Object.rawValue
block2.physicsBody!.collisionBitMask = ColliderType.Object.rawValue

block2.zPosition = -2

self.addChild(block2)

let gap = SKNode()

gap.position = CGPoint(x: self.frame.midX + self.frame.width, y: self.frame.midY + blockOffset)

gap.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: blockTexture.size().width, height: gapHeight))

gap.physicsBody!.isDynamic = false

gap.run(moveBlocks)

gap.physicsBody!.contactTestBitMask = ColliderType.santa.rawValue
gap.physicsBody!.categoryBitMask = ColliderType.Gap.rawValue
gap.physicsBody!.collisionBitMask = ColliderType.Gap.rawValue

self.addChild(gap)

}

func didBegin(_ contact: SKPhysicsContact) {

if gameOver == false {

if contact.bodyA.categoryBitMask == ColliderType.Gap.rawValue || contact.bodyB.categoryBitMask == ColliderType.Gap.rawValue {

score += 1

scoreLabel.text = String(score)


} else {

self.speed = 0

gameOver = true

timer.invalidate()

let gameOverScreenTexture = SKTexture(imageNamed: "background.jpg")

var j: CGFloat = 0

gameOverScreen = SKSpriteNode(texture: gameOverScreenTexture, size: CGSize(width: 600, height: 600))

gameOverScreen.position = CGPoint(x: self.frame.midX, y: self.frame.midY)

gameOverScreen.size.height = self.frame.height / 3

gameOverScreen.zPosition = -1

self.addChild(gameOverScreen)


//share button

let shareButtonTexture = SKTexture(imageNamed: "shareButton.png")

var k: CGFloat = 0

shareButton = SKSpriteNode(texture: shareButtonTexture, size: CGSize(width: 100, height: 100))

shareButton.position = CGPoint(x: self.frame.midX, y: self.frame.midY / 3)

shareButton.size.height = self.frame.height / 6

shareButton.zPosition = 0

self.addChild(shareButton)


}
}

}

func openShareMenu(value: String, image: UIImage?) {
guard let view = view else { return }

// Activity items
var activityItems = [AnyObject]()

// Text
let text = "Can you beat my score "
activityItems.append(text as AnyObject)

// Add image if valid
if let image = image {
activityItems.append(image)
}

// Activity controller
let activityController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)


// Excluded activity types
activityController.excludedActivityTypes = [
UIActivityType.airDrop,
UIActivityType.print,
UIActivityType.assignToContact,
UIActivityType.addToReadingList,
]

// Present
view.window?.rootViewController?.present(activityController, animated: true)
}



override func didMove(to view: SKView) {

addChild(shareButton)

self.physicsWorld.contactDelegate = self

setupGame()

}

func setupGame() {

timer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(self.makeBlocks), userInfo: nil, repeats: true)

let bgTexture = SKTexture(imageNamed: "bg.png")

let moveBGAnimation = SKAction.move(by: CGVector(dx: -bgTexture.size().width, dy: 0), duration: 7)
let shiftBGAnimation = SKAction.move(by: CGVector(dx: bgTexture.size().width, dy: 0), duration: 0)
let moveBGForever = SKAction.repeatForever(SKAction.sequence([moveBGAnimation, shiftBGAnimation]))

var i: CGFloat = 0

while i < 3 {

bg = SKSpriteNode(texture: bgTexture)

bg.position = CGPoint(x: bgTexture.size().width * i, y: self.frame.midY)

bg.size.height = self.frame.height

bg.run(moveBGForever)

bg.zPosition = -3

self.addChild(bg)

i += 1

}


let santaTexture = SKTexture(imageNamed: "santa1.png")
let santaTexture2 = SKTexture(imageNamed: "santa2.png")

let animation = SKAction.animate(with: [santaTexture, santaTexture2], timePerFrame: 0.1)
let makeSantaMove = SKAction.repeatForever(animation)

santa = SKSpriteNode(texture: santaTexture)

santa.position = CGPoint(x: self.frame.midX, y: self.frame.midY)

santa.run(makeSantaMove)

santa.physicsBody = SKPhysicsBody(circleOfRadius: santaTexture.size().height / 2)

santa.physicsBody!.isDynamic = false

santa.physicsBody!.contactTestBitMask = ColliderType.Object.rawValue
santa.physicsBody!.categoryBitMask = ColliderType.santa.rawValue
santa.physicsBody!.collisionBitMask = ColliderType.santa.rawValue

self.addChild(santa)

let ground = SKNode()

ground.position = CGPoint(x: self.frame.midX, y: -self.frame.height / 2)

ground.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: self.frame.width, height: 1))

ground.physicsBody!.isDynamic = false

ground.physicsBody!.contactTestBitMask = ColliderType.Object.rawValue
ground.physicsBody!.categoryBitMask = ColliderType.Object.rawValue
ground.physicsBody!.collisionBitMask = ColliderType.Object.rawValue


self.addChild(ground)

scoreLabel.fontName = "Helvetica"

scoreLabel.fontSize = 60

scoreLabel.text = "0"

scoreLabel.position = CGPoint(x: self.frame.midX, y: self.frame.height / 2 - 220)

self.addChild(scoreLabel)

tapToPlayLabel.fontName = "Helvetica"

tapToPlayLabel.fontSize = 70

tapToPlayLabel.text = "Tap to Play!"

tapToPlayLabel.position = CGPoint(x: self.frame.midX, y: self.frame.height / 2 - 500)

self.addChild(tapToPlayLabel)


}

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

if gameOver == false {

tapToPlayLabel.isHidden = true

santa.physicsBody!.isDynamic = true

santa.physicsBody!.velocity = CGVector(dx: 0, dy: 0)

santa.physicsBody!.applyImpulse(CGVector(dx: 0, dy: 320))

} else {

tapToPlayLabel.isHidden = false

gameOver = false

score = 0

self.speed = 1

self.removeAllChildren()

setupGame()

}

}

override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
}

}

Answer

This seems to be quite a common problem people have with SpriteKit games so lets go through the difference between SpriteKit games and UIKit apps.

When you make a regular UIKit app, e.g. YouTube, Facebook, you would use ViewControllers, CollectionViews, Views etc for each screen/menu that you see (Home screen, Channel screen, Subscription channel screen etc). So you would use UIKit APIs for this such as UIButtons, UIImageViews, UILabels, UIViews, UICollectionViews etc. To do this visually we would use storyboards.

In SpriteKit games on the other hand it works differently. You work with SKScenes for each screen that you see (MenuScene, SettingsScene, GameScene, GameOverScene etc) and only have 1 ViewController (GameViewController). That GameViewController, which has a SKView in it, will present all your SKScenes.

So we should add our UI directly in the relevant SKScenes using SpriteKit APIs such as SKLabelNodes, SKSpriteNodes, SKNodes etc. To do this visually we would use the SpriteKit scene level editor and not storyboards.

So the general logic would be to load your 1st SKScene as usual from the GameViewController and than do the rest from within the relevant SKScenes. Your GameViewController should basically have next to no code in it beyond the default code. You can also transition from 1 scene to another scene very easily (GameScene -> GameOverScene).

If you use GameViewController for your UI it will get messy really quickly if you have multiple SKScenes because UI will be added to GameViewController and therefore all SKScenes. So you would have to remove/show UI when you transition between scenes and it would be madness.

To add a label in SpriteKit it would be something like this

 class GameScene: SKScene {

  lazy var scoreLabel: SKLabelNode = {
      let label = SKLabelNode(fontNamed: "HelveticaNeue")
      label.text = "SomeText"
      label.fontSize = 22
      label.fontColor = .yellow
      label.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
      return label
  }()

  override func didMove(to view: SKView) {

      addChild(scoreLabel)
  }
} 

To make buttons you essentially create a SKSpriteNode and give it a name and then look for it in touchesBegan or touchesEnded and run an SKAction on it for animation and some code after.

enum ButtonName: String {
     case play
     case share
}

class GameScene: SKScene {

       lazy var shareButton: SKSpriteNode = {
            let button = SKSpriteNode(imageNamed: "ShareButton")
            button.name = ButtonName.share.rawValue
            button.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
            return button
        }()

       override func didMove(to view: SKView) {

             addChild(shareButton)
       }

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

             for touch in touches {
                 let location = touch.location(in: self)
                 let node = atPoint(location)

                 if let nodeName = node.name {
                      switch nodeName {
                      case ButtonName.play.rawValue:
                          // run some SKAction animation and some code
                      case ButtonName.share.rawValue:

                          let action1 = SKAction.scale(to: 0.9, duration: 0.2)
                          let action2 = SKAction.scale(to: 1, duration: 0.2)
                          let action3 = SKAction.run { [weak self] in
                             self?.openShareMenu(value: "\(self!.score)", image: nil) // image is nil in this example, if you use a image just create a UIImage and pass it into the method
                          }
                          let sequence = SKAction.sequence([action1, action2, action3])
                          node.run(sequence)

                      default: 
                          break
                    }
               }
          }
     }
} 

To make this even easier I would create a button helper class, for a simple example have a look at this https://nathandemick.com/2014/09/buttons-sprite-kit-using-swift/

You can also check out Apple's sample game DemoBots for a more feature rich example.

This way you can have things such as animations etc in the helper class and don't have to repeat code for each button.

For sharing, I would actually use UIActivityController instead of those older Social APIs which might become deprecated soon. This also allows you to share to multiple services via 1 UI and you will also only need 1 share button in your app. It could be a simple function like this in the SKScene you are calling it from.

func openShareMenu(value: String, image: UIImage?) {
    guard let view = view else { return }

    // Activity items
    var activityItems = [AnyObject]()

    // Text
    let text = "Can you beat my score " + value
    activityItems.append(text as AnyObject)

    // Add image if valid
    if let image = image {
        activityItems.append(image)
    }

    // Activity controller
    let activityController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)

    // iPad settings
    if Device.isPad {
        activityController.popoverPresentationController?.sourceView = view
        activityController.popoverPresentationController?.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0)
        activityController.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection.init(rawValue: 0)
    }

    // Excluded activity types
    activityController.excludedActivityTypes = [
        UIActivityType.airDrop,
        UIActivityType.print,
        UIActivityType.assignToContact,
        UIActivityType.addToReadingList,
    ]

    // Present
    view.window?.rootViewController?.present(activityController, animated: true)
}

and then call it like so when the correct button was pressed (see above example)

openShareMenu(value: "\(self.score)", image: SOMEUIIMAGE)

Hope this helps