SpriteKit框架详细解析(八) —— 基于SpriteKit的类Cut the Rope游戏简单示例(二)

版本记录

版本号 时间
V1.0 2019.10.26 星期六

前言

SpriteKit框架使用优化的动画系统,物理模拟和事件处理支持创建基于2D精灵的游戏。接下来这几篇我们就详细的解析一下这个框架。相关代码已经传至GitHub - 刀客传奇,感兴趣的可以阅读另外几篇文章。
1. SpriteKit框架详细解析(一) —— 基本概览(一)
2. SpriteKit框架详细解析(二) —— 一个简单的动画实例(一)
3. SpriteKit框架详细解析(三) —— 创建一个简单的2D游戏(一)
4. SpriteKit框架详细解析(四) —— 创建一个简单的2D游戏(二)
5. SpriteKit框架详细解析(五) —— 基于SpriteKit的游戏编程的三角函数(一)
6. SpriteKit框架详细解析(六) —— 基于SpriteKit的游戏编程的三角函数(二)
7. SpriteKit框架详细解析(七) —— 基于SpriteKit的类Cut the Rope游戏简单示例(一)

开始

题外话:周一就去成都出差了,祝自己一路顺风吧~~

1. Swift

首先看下工程组织结构

接着就是sb中的内容了

接着就是源码了

1. Constants.swift
import CoreGraphics

enum ImageName {
  static let background = "Background"
  static let ground = "Ground"
  static let water = "Water"
  static let vineTexture = "VineTexture"
  static let vineHolder = "VineHolder"
  static let crocMouthClosed = "CrocMouthClosed"
  static let crocMouthOpen = "CrocMouthOpen"
  static let crocMask = "CrocMask"
  static let prize = "Pineapple"
  static let prizeMask = "PineappleMask"
}

enum SoundFile {
  static let backgroundMusic = "CheeZeeJungle.caf"
  static let slice = "Slice.caf"
  static let splash = "Splash.caf"
  static let nomNom = "NomNom.caf"
}

enum Layer {
  static let background: CGFloat = 0
  static let crocodile: CGFloat = 1
  static let vine: CGFloat = 1
  static let prize: CGFloat = 2
  static let foreground: CGFloat = 3
}

enum PhysicsCategory {
  static let crocodile: UInt32 = 1
  static let vineHolder: UInt32 = 2
  static let vine: UInt32 = 4
  static let prize: UInt32 = 8
}

enum GameConfiguration {
  static let vineDataFile = "VineData.plist"
  static let canCutMultipleVinesAtOnce = false
}

enum Scene {
  static let particles = "Particle.sks"
}
2. GameViewController.swift
import UIKit
import SpriteKit
import GameplayKit

class GameViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    
    // Configure the view.
    let skView = self.view as! SKView
    skView.showsFPS = true
    skView.showsNodeCount = true
    skView.ignoresSiblingOrder = true
    
    // Create and configure the scene.
    let scene = GameScene(size: CGSize(width: 375, height: 667))
    scene.scaleMode = .aspectFill
    
    // Present the scene.
    skView.presentScene(scene)
  }
}
3. GameScene.swift
import SpriteKit
import AVFoundation

class GameScene: SKScene {
  private var particles: SKEmitterNode?
  private var crocodile: SKSpriteNode!
  private var prize: SKSpriteNode!
  
  private static var backgroundMusicPlayer: AVAudioPlayer!
  
  private var sliceSoundAction: SKAction!
  private var splashSoundAction: SKAction!
  private var nomNomSoundAction: SKAction!
  
  private var isLevelOver = false
  private var didCutVine = false
  
  override func didMove(to view: SKView) {
    setUpPhysics()
    setUpScenery()
    setUpPrize()
    setUpVines()
    setUpCrocodile()
    setUpAudio()
  }
  
  //MARK: - Level setup
  
  private func setUpPhysics() {
    physicsWorld.contactDelegate = self
    physicsWorld.gravity = CGVector(dx: 0.0, dy: -9.8)
    physicsWorld.speed = 1.0
  }
  
  private func setUpScenery() {
    let background = SKSpriteNode(imageNamed: ImageName.background)
    background.anchorPoint = CGPoint(x: 0, y: 0)
    background.position = CGPoint(x: 0, y: 0)
    background.zPosition = Layer.background
    background.size = CGSize(width: size.width, height: size.height)
    addChild(background)
    
    let water = SKSpriteNode(imageNamed: ImageName.water)
    water.anchorPoint = CGPoint(x: 0, y: 0)
    water.position = CGPoint(x: 0, y: 0)
    water.zPosition = Layer.foreground
    water.size = CGSize(width: size.width, height: size.height * 0.2139)
    addChild(water)
  }
  
  private func setUpPrize() {
    prize = SKSpriteNode(imageNamed: ImageName.prize)
    prize.position = CGPoint(x: size.width * 0.5, y: size.height * 0.7)
    prize.zPosition = Layer.prize
    prize.physicsBody = SKPhysicsBody(circleOfRadius: prize.size.height / 2)
    prize.physicsBody?.categoryBitMask = PhysicsCategory.prize
    prize.physicsBody?.collisionBitMask = 0
    prize.physicsBody?.density = 0.5

    addChild(prize)
  }
  
  //MARK: - Vine methods
  
  private func setUpVines() {
    // load vine data
    let decoder = PropertyListDecoder()
    guard
      let dataFile = Bundle.main.url(
        forResource: GameConfiguration.vineDataFile,
        withExtension: nil),
      let data = try? Data(contentsOf: dataFile),
      let vines = try? decoder.decode([VineData].self, from: data)
    else {
      return
    }

    for (i, vineData) in vines.enumerated() {
      let anchorPoint = CGPoint(
        x: vineData.relAnchorPoint.x * size.width,
        y: vineData.relAnchorPoint.y * size.height)
      let vine = VineNode(length: vineData.length, anchorPoint: anchorPoint, name: "\(i)")

      vine.addToScene(self)

      vine.attachToPrize(prize)
    }
  }
  
  //MARK: - Croc methods
  
  private func setUpCrocodile() {
    crocodile = SKSpriteNode(imageNamed: ImageName.crocMouthClosed)
    crocodile.position = CGPoint(x: size.width * 0.75, y: size.height * 0.312)
    crocodile.zPosition = Layer.crocodile
    crocodile.physicsBody = SKPhysicsBody(
      texture: SKTexture(imageNamed: ImageName.crocMask),
      size: crocodile.size)
    crocodile.physicsBody?.categoryBitMask = PhysicsCategory.crocodile
    crocodile.physicsBody?.collisionBitMask = 0
    crocodile.physicsBody?.contactTestBitMask = PhysicsCategory.prize
    crocodile.physicsBody?.isDynamic = false
        
    addChild(crocodile)
        
    animateCrocodile()
  }
  
  private func animateCrocodile() {
    let duration = Double.random(in: 2...4)
    let open = SKAction.setTexture(SKTexture(imageNamed: ImageName.crocMouthOpen))
    let wait = SKAction.wait(forDuration: duration)
    let close = SKAction.setTexture(SKTexture(imageNamed: ImageName.crocMouthClosed))
    let sequence = SKAction.sequence([wait, open, wait, close])
        
    crocodile.run(.repeatForever(sequence))
  }
  
  private func runNomNomAnimation(withDelay delay: TimeInterval) {
    crocodile.removeAllActions()

    let closeMouth = SKAction.setTexture(SKTexture(imageNamed: ImageName.crocMouthClosed))
    let wait = SKAction.wait(forDuration: delay)
    let openMouth = SKAction.setTexture(SKTexture(imageNamed: ImageName.crocMouthOpen))
    let sequence = SKAction.sequence([closeMouth, wait, openMouth, wait, closeMouth])

    crocodile.run(sequence)
  }
  
  //MARK: - Touch handling

  override func touchesBegan(_ touches: Set, with event: UIEvent?) {
    didCutVine = false
  }
  
  override func touchesMoved(_ touches: Set, with event: UIEvent?) {
    for touch in touches {
      let startPoint = touch.location(in: self)
      let endPoint = touch.previousLocation(in: self)
      
      // check if vine cut
      scene?.physicsWorld.enumerateBodies(
        alongRayStart: startPoint,
        end: endPoint,
        using: { body, _, _, _ in
          self.checkIfVineCut(withBody: body)
      })
      
      // produce some nice particles
      showMoveParticles(touchPosition: startPoint)
    }
  }
  
  override func touchesEnded(_ touches: Set, with event: UIEvent?) {
    particles?.removeFromParent()
    particles = nil
  }
  
  private func showMoveParticles(touchPosition: CGPoint) {
    if particles == nil {
      particles = SKEmitterNode(fileNamed: Scene.particles)
      particles!.zPosition = 1
      particles!.targetNode = self
      addChild(particles!)
    }
    particles!.position = touchPosition
  }
  
  //MARK: - Game logic
  
  private func checkIfVineCut(withBody body: SKPhysicsBody) {
    if didCutVine && !GameConfiguration.canCutMultipleVinesAtOnce {
      return
    }
    
    let node = body.node!

    // if it has a name it must be a vine node
    if let name = node.name {
      // snip the vine
      node.removeFromParent()

      // fade out all nodes matching name
      enumerateChildNodes(withName: name, using: { node, _ in
        let fadeAway = SKAction.fadeOut(withDuration: 0.25)
        let removeNode = SKAction.removeFromParent()
        let sequence = SKAction.sequence([fadeAway, removeNode])
        node.run(sequence)
      })
      
      crocodile.removeAllActions()
      crocodile.texture = SKTexture(imageNamed: ImageName.crocMouthOpen)
      animateCrocodile()
      run(sliceSoundAction)
      didCutVine = true
    }
  }
  
  private func switchToNewGame(withTransition transition: SKTransition) {
    let delay = SKAction.wait(forDuration: 1)
    let sceneChange = SKAction.run {
      let scene = GameScene(size: self.size)
      self.view?.presentScene(scene, transition: transition)
    }

    run(.sequence([delay, sceneChange]))
  }
  
  //MARK: - Audio
  
  private func setUpAudio() {
    if GameScene.backgroundMusicPlayer == nil {
      let backgroundMusicURL = Bundle.main.url(
        forResource: SoundFile.backgroundMusic,
        withExtension: nil)
      
      do {
        let theme = try AVAudioPlayer(contentsOf: backgroundMusicURL!)
        GameScene.backgroundMusicPlayer = theme
      } catch {
        // couldn't load file :[
      }
      
      GameScene.backgroundMusicPlayer.numberOfLoops = -1
    }
    
    if !GameScene.backgroundMusicPlayer.isPlaying {
      GameScene.backgroundMusicPlayer.play()
    }
    
    sliceSoundAction = .playSoundFileNamed(
      SoundFile.slice,
      waitForCompletion: false)
    splashSoundAction = .playSoundFileNamed(
      SoundFile.splash,
      waitForCompletion: false)
    nomNomSoundAction = .playSoundFileNamed(
      SoundFile.nomNom,
      waitForCompletion: false)
  }
}

extension GameScene: SKPhysicsContactDelegate {
  override func update(_ currentTime: TimeInterval) {
    if isLevelOver {
      return
    }
    
    if prize.position.y <= 0 {
      isLevelOver = true
      run(splashSoundAction)
      switchToNewGame(withTransition: .fade(withDuration: 1.0))
    }
  }
  
  func didBegin(_ contact: SKPhysicsContact) {
    if isLevelOver {
      return
    }

    if (contact.bodyA.node == crocodile && contact.bodyB.node == prize)
      || (contact.bodyA.node == prize && contact.bodyB.node == crocodile) {
      
      isLevelOver = true
      
      // shrink the pineapple away
      let shrink = SKAction.scale(to: 0, duration: 0.08)
      let removeNode = SKAction.removeFromParent()
      let sequence = SKAction.sequence([shrink, removeNode])
      prize.run(sequence)
      run(nomNomSoundAction)
      runNomNomAnimation(withDelay: 0.15)
      // transition to next level
      switchToNewGame(withTransition: .doorway(withDuration: 1.0))
    }
  }
}
4. VineNode.swift
import UIKit
import SpriteKit

class VineNode: SKNode {
  private let length: Int
  private let anchorPoint: CGPoint
  private var vineSegments: [SKNode] = []
  
  init(length: Int, anchorPoint: CGPoint, name: String) {
    self.length = length
    self.anchorPoint = anchorPoint

    super.init()

    self.name = name
  }
  
  required init?(coder aDecoder: NSCoder) {
    length = aDecoder.decodeInteger(forKey: "length")
    anchorPoint = aDecoder.decodeCGPoint(forKey: "anchorPoint")

    super.init(coder: aDecoder)
  }
  
  func addToScene(_ scene: SKScene) {
    // add vine to scene
    zPosition = Layer.vine
    scene.addChild(self)
    
    // create vine holder
    let vineHolder = SKSpriteNode(imageNamed: ImageName.vineHolder)
    vineHolder.position = anchorPoint
    vineHolder.zPosition = 1
        
    addChild(vineHolder)
        
    vineHolder.physicsBody = SKPhysicsBody(circleOfRadius: vineHolder.size.width / 2)
    vineHolder.physicsBody?.isDynamic = false
    vineHolder.physicsBody?.categoryBitMask = PhysicsCategory.vineHolder
    vineHolder.physicsBody?.collisionBitMask = 0
    
    // add each of the vine parts
    for i in 0..
5. VineData.swift
import UIKit

struct VineData: Decodable {
  let length: Int
  let relAnchorPoint: CGPoint
}

后记

本篇主要讲述了基于SpriteKit的类Cut the Rope游戏简单示例,感兴趣的给个赞或者关注~~~

你可能感兴趣的:(SpriteKit框架详细解析(八) —— 基于SpriteKit的类Cut the Rope游戏简单示例(二))