Core Haptics框架详细解析(三) —— 一个简单示例(二)

版本记录

版本号 时间
V1.0 2020.08.07 星期五

前言

Core HapticsiOS13的新的SDK,接下来几篇我们就一起看一下这个专题。感兴趣的可以看下面几篇文章。
1. Core Haptics框架详细解析(一) —— 基本概览(一)
2. Core Haptics框架详细解析(二) —— 一个简单示例(一)

源码

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. 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..
3. GameViewController.swift
import UIKit
import SpriteKit
import GameplayKit

class GameViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()

    // Configure the view.
    // swiftlint:disable:next force_cast
    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)
  }
}
4. VineData.swift
import UIKit

struct VineData: Decodable {
  let length: Int
  let relAnchorPoint: CGPoint
}
5. GameScene.swift
import SpriteKit
import AVFoundation
import CoreHaptics

class GameScene: SKScene {
  // swiftlint:disable implicitly_unwrapped_optional
  private var particles: SKEmitterNode?
  private var crocodile: SKSpriteNode!
  private var prize: SKSpriteNode!

  private var hapticManager: HapticManager?
  private static var backgroundMusicPlayer: AVAudioPlayer!

  private var sliceSoundAction: SKAction!
  private var splashSoundAction: SKAction!
  private var nomNomSoundAction: SKAction!
  // swiftlint:enable implicitly_unwrapped_optional

  private var isLevelOver = false
  private var didCutVine = false

  private var swishTimestamp: TimeInterval = 0

  override func didMove(to view: SKView) {
    hapticManager = HapticManager()
    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
    hapticManager?.startSwishPlayer()
  }

  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) { body, _, _, _ in
          self.checkIfVineCut(withBody: body)
      }

      // produce some nice particles
      showMoveParticles(touchPosition: startPoint)

      // update haptic player intensity
      let distance = CGVector(dx: abs(startPoint.x - endPoint.x), dy: abs(startPoint.y - endPoint.y))
      let distanceRatio = CGVector(dx: distance.dx / size.width, dy: distance.dy / size.height)
      let intensity = Float(max(distanceRatio.dx, distanceRatio.dy)) * 100
      hapticManager?.updateSwishPlayer(intensity: intensity)
    }
  }

  override func touchesEnded(_ touches: Set, with event: UIEvent?) {
    particles?.removeFromParent()
    particles = nil
    hapticManager?.stopSwishPlayer()
  }

  private func showMoveParticles(touchPosition: CGPoint) {
    // swiftlint:disable force_unwrapping
    if particles == nil {
      particles = SKEmitterNode(fileNamed: Scene.particles)
      particles!.zPosition = 1
      particles!.targetNode = self
      addChild(particles!)
    }
    particles!.position = touchPosition
    // swiftlint:enable force_unwrapping
  }

  // MARK: - Game logic

  private func checkIfVineCut(withBody body: SKPhysicsBody) {
    if didCutVine && !GameConfiguration.canCutMultipleVinesAtOnce {
      return
    }

    guard let node = body.node else {
      return
    }

    // 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) { 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 & Haptics

  private func setUpAudio() {
    if GameScene.backgroundMusicPlayer == nil {
      guard let backgroundMusicURL = Bundle.main.url(
        forResource: SoundFile.backgroundMusic,
        withExtension: nil) else {
          return
      }

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

    guard let manager = hapticManager else {
      sliceSoundAction = .playSoundFileNamed(
        SoundFile.slice,
        waitForCompletion: false)
      nomNomSoundAction = .playSoundFileNamed(
        SoundFile.nomNom,
        waitForCompletion: false)
      splashSoundAction = .playSoundFileNamed(
        SoundFile.splash,
        waitForCompletion: false)
      return
    }

    setupHaptics(manager)
  }

  private func setupHaptics(_ manager: HapticManager) {
    let sliceHaptics = SKAction.run {
      manager.playSlice()
    }
    if manager.sliceAudio != nil {
      sliceSoundAction = sliceHaptics
    } else {
      sliceSoundAction = .group([
        .playSoundFileNamed(SoundFile.slice, waitForCompletion: false),
        sliceHaptics
      ])
    }

    let nomNomHaptics = SKAction.run {
      manager.playNomNom()
    }
    if manager.nomNomAudio != nil {
      nomNomSoundAction = nomNomHaptics
    } else {
      nomNomSoundAction = .group([
        .playSoundFileNamed(SoundFile.nomNom, waitForCompletion: false),
        nomNomHaptics
      ])
    }

    let splashHaptics = SKAction.run {
      manager.playSplash()
    }
    if manager.splashAudio != nil {
      splashSoundAction = splashHaptics
    } else {
      splashSoundAction = .group([
        .playSoundFileNamed(SoundFile.splash, waitForCompletion: false),
        splashHaptics
      ])
    }
  }
}

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))
    }
  }
}
6. Haptics.swift
import CoreHaptics

class HapticManager {
  let hapticEngine: CHHapticEngine
  var sliceAudio: CHHapticAudioResourceID?
  var nomNomAudio: CHHapticAudioResourceID?
  var splashAudio: CHHapticAudioResourceID?
  var swishPlayer: CHHapticAdvancedPatternPlayer?

  // Failable initializer: the game will ignore haptics if the manager is nil

  init?() {
    // Check if the device supports haptics and fail the initializer if it doesn't
    let hapticCapability = CHHapticEngine.capabilitiesForHardware()
    guard hapticCapability.supportsHaptics else {
      return nil
    }

    // Try to ceate the engine, fail the initializer if it fails
    do {
      hapticEngine = try CHHapticEngine()
    } catch let error {
      print("Haptic engine Creation Error: \(error)")
      return nil
    }

    do {
      try hapticEngine.start()
    } catch let error {
      print("Haptic failed to start Error: \(error)")
    }

    hapticEngine.isAutoShutdownEnabled = true

    hapticEngine.resetHandler = { [weak self] in
      self?.handleEngineReset()
    }

    // Setup our audio resources
    setupResources()
  }

  private func handleEngineReset() {
    print("Engine is resetting...")
    do {
      try hapticEngine.start()
      setupResources()
      createSwishPlayer()
    } catch {
      print("Failed to restart the engine: \(error)")
    }
  }

  private func setupResources() {
    do {
      if let path = Bundle.main.url(forResource: "Slice", withExtension: "caf") {
        sliceAudio = try hapticEngine.registerAudioResource(path)
      }
      if let path = Bundle.main.url(forResource: "NomNom", withExtension: "caf") {
        nomNomAudio = try hapticEngine.registerAudioResource(path)
      }
      if let path = Bundle.main.url(forResource: "Splash", withExtension: "caf") {
        splashAudio = try hapticEngine.registerAudioResource(path)
      }
    } catch {
      print("Failed to load audio: \(error)")
    }

    createSwishPlayer()
  }

  // MARK: - Dynamic Swish Player

  private func createSwishPlayer() {
    let swish = CHHapticEvent(
      eventType: .hapticContinuous,
      parameters: [
        CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5),
        CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
      ],
      relativeTime: 0,
      duration: 60)

    do {
      let pattern = try CHHapticPattern(events: [swish], parameters: [])
      swishPlayer = try hapticEngine.makeAdvancedPlayer(with: pattern)
    } catch let error {
      print("Swish player error: \(error)")
    }
  }

  func startSwishPlayer() {
    do {
      try hapticEngine.start()
      try swishPlayer?.start(atTime: CHHapticTimeImmediate)
    } catch {
      print("Swish player start error: \(error)")
    }
  }

  func stopSwishPlayer() {
    do {
      try swishPlayer?.stop(atTime: CHHapticTimeImmediate)
    } catch {
      print("Swish player stop error: \(error)")
    }
  }

  func updateSwishPlayer(intensity: Float) {
    let intensity = CHHapticDynamicParameter(
      parameterID: .hapticIntensityControl,
      value: intensity,
      relativeTime: 0)
    do {
      try swishPlayer?.sendParameters([intensity], atTime: CHHapticTimeImmediate)
    } catch let error {
      print("Swish player dynamic update error: \(error)")
    }
  }

  // MARK: - Play Haptic Patterns

  func playSlice() {
    do {
      let pattern = try slicePattern()
      try playHapticFromPattern(pattern)
    } catch {
      print("Failed to play slice: \(error)")
    }
  }

  func playNomNom() {
    do {
      let pattern = try nomNomPattern()
      try playHapticFromPattern(pattern)
    } catch {
      print("Failed to play nomNom: \(error)")
    }
  }

  func playSplash() {
    do {
      let pattern = try splashPattern()
      try playHapticFromPattern(pattern)
    } catch {
      print("Failed to play splash: \(error)")
    }
  }

  private func playHapticFromPattern(_ pattern: CHHapticPattern) throws {
    try hapticEngine.start()
    let player = try hapticEngine.makePlayer(with: pattern)
    try player.start(atTime: CHHapticTimeImmediate)
  }
}

// MARK: - Haptic Patterns

extension HapticManager {
  private func slicePattern() throws -> CHHapticPattern {
    let slice = CHHapticEvent(
      eventType: .hapticContinuous,
      parameters: [
        CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6),
        CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8)
      ],
      relativeTime: 0,
      duration: 0.5)

    let snip = CHHapticEvent(
      eventType: .hapticTransient,
      parameters: [
        CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
        CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
      ],
      relativeTime: 0.08)

    let curve = CHHapticParameterCurve(
      parameterID: .hapticIntensityControl,
      controlPoints: [
        .init(relativeTime: 0, value: 0.2),
        .init(relativeTime: 0.08, value: 1.0),
        .init(relativeTime: 0.24, value: 0.2),
        .init(relativeTime: 0.34, value: 0.6),
        .init(relativeTime: 0.5, value: 0)
      ],
      relativeTime: 0)

    var events = [slice, snip]
    if let audioResourceID = sliceAudio {
      let audio = CHHapticEvent(audioResourceID: audioResourceID, parameters: [], relativeTime: 0)
      events.append(audio)
    }

    return try CHHapticPattern(events: events, parameterCurves: [curve])
  }

  private func nomNomPattern() throws -> CHHapticPattern {
    let rumble1 = CHHapticEvent(
      eventType: .hapticContinuous,
      parameters: [
        CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
        CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
      ],
      relativeTime: 0,
      duration: 0.15)

    let rumble2 = CHHapticEvent(
      eventType: .hapticContinuous,
      parameters: [
        CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
        CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1)
      ],
      relativeTime: 0.3,
      duration: 0.3)

    let crunch1 = CHHapticEvent(
      eventType: .hapticTransient,
      parameters: [
        CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
        CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
      ],
      relativeTime: 0)

    let crunch2 = CHHapticEvent(
      eventType: .hapticTransient,
      parameters: [
        CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
        CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
      ],
      relativeTime: 0.3)

    var events = [rumble1, rumble2, crunch1, crunch2]
    if let audioResourceID = nomNomAudio {
      let audio = CHHapticEvent(audioResourceID: audioResourceID, parameters: [], relativeTime: 0)
      events.append(audio)
    }

    return try CHHapticPattern(events: events, parameters: [])
  }

  private func splashPattern() throws -> CHHapticPattern {
    let splish = CHHapticEvent(
      eventType: .hapticTransient,
      parameters: [
        CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
        CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1)
      ],
      relativeTime: 0)

    let splash = CHHapticEvent(
      eventType: .hapticContinuous,
      parameters: [
        CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5),
        CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1),
        CHHapticEventParameter(parameterID: .attackTime, value: 0.1),
        CHHapticEventParameter(parameterID: .releaseTime, value: 0.2),
        CHHapticEventParameter(parameterID: .decayTime, value: 0.3)
      ],
      relativeTime: 0.1,
      duration: 0.6)

    var events = [splish, splash]
    if let audioResourceID = splashAudio {
      let audio = CHHapticEvent(audioResourceID: audioResourceID, parameters: [], relativeTime: 0)
      events.append(audio)
    }

    return try CHHapticPattern(events: events, parameters: [])
  }
}

后记

本篇主要讲述了Core Haptics的一个简单示例,感兴趣的给个赞或者关注~~~

你可能感兴趣的:(Core Haptics框架详细解析(三) —— 一个简单示例(二))