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

版本记录

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

前言

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

开始

首先看下主要内容:

在本Core Haptics教程中,您将学习如何创建和播放触觉模式,将音频与触觉事件同步以及如何创建可响应外部刺激的动态触觉模式。内容来自翻译

下面看下写作环境:

Swift 5, iOS 13, Xcode 11

接着就是正文啦

AppleiOS 13中引入了Core Haptics。这是一个新的API,可以生成自定义的高分辨率触觉和音频反馈,使振动和隆隆声听起来像老式的头。 准备将自己沉浸在触觉体验的世界中。

在本教程中,您将通过新的触觉(haptic)体验来增强应用程序。 您将学习如何:

  • 1) 创建并播放触觉模式。
  • 2) 将音频与触觉事件同步。
  • 3) 创建动态触觉模式,这些模式会响应外部刺激而发生变化。

注意:本中级教程假定您可以轻松使用Xcode构建iOS应用并编写Swift。 您还需要一台运行iOS 13并支持触觉反馈的设备。 本教程使用教程 How To Make a Game Like Cut the Rope With SpriteKit中的游戏作为开始项目。 如果您想了解SpriteKit的介绍,请先阅读该教程。

打开起始项目并运行,你会看到游戏:

您拥有可爱的图形,物理模拟和动画流畅,声音效果将玩家置于丛林中。但是您会强烈感觉到缺少某些东西。切菠萝时还有就是落入水中或者鳄鱼吃的时候没有声音。

是时候使用Core Haptics了!


Adding Your First Haptic Experience

最好的开始方法是快速进行简单的触觉体验并对其进行测试。您的第一次触觉体验将为玩家切开藤本时的感觉提供一些声音效果。

创建触觉体验的标准过程是:

  • 1) 检查设备兼容性。
  • 2) 创建一个触觉引擎对象。
  • 3) 创建触觉事件模式。
  • 4) 做一个模式玩家。
  • 5) 启动触觉(haptic)引擎。
  • 6) 播放模式。
  • 7) 停止触觉(haptic)引擎。

为避免向GameScene.swift添加更多代码,请展开SnipTheVine ▸ Classes,然后查找并打开Haptics.swift。这是一个空文件供您使用。添加以下内容:

import CoreHaptics

class HapticManager {
  // 1
  let hapticEngine: CHHapticEngine

  // 2
  init?() {
    // 3
    let hapticCapability = CHHapticEngine.capabilitiesForHardware()
    guard hapticCapability.supportsHaptics else {
      return nil
    }

    // 4
    do {
      hapticEngine = try CHHapticEngine()
    } catch let error {
      print("Haptic engine Creation Error: \(error)")
      return nil
    }
  }
}

此代码的作用如下:

  • 1) hapticEngine拥有对CHHapticEngine的引用。
  • 2) 初始化程序会失败,因此您可以检查游戏场景中的初始化程序是否为nil,并使用它来指示触觉不可用。
  • 3) 在初始化程序中要做的第一件事是检查触觉是否可用。 调用CHHapticEngine.capabilitiesForHardware()获得一个对象,您可以通过简单检查supportsHaptics来测试该对象。
  • 4) 最后,创建一个引擎对象。 CHHapticEngine初始化程序可以抛出异常,因此您需要将其包装在do / catch块中,如果引发错误,则返回nil

注意:导致触觉在设备上不可用的原因有很多,因此您需要为许多API使用do-catch块。 这也意味着您需要确保对任何触觉体验都有一个fallback

现在,继续前进,直到可以尽快测试您的第一个触觉为止。

仍在Haptics.swift中,将此扩展添加到类中:

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

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

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

slicePattern()返回您的第一个Core Haptics模式。 它创建两个触觉事件和使用它们的触觉模式。 您将很快探索事件和模式的详细信息,但是现在,继续前进吧!

最后,向HapticManager添加方法以播放模式:

func playSlice() {
  do {
    // 1
    let pattern = try slicePattern()
    // 2
    try hapticEngine.start()
    // 3
    let player = try hapticEngine.makePlayer(with: pattern)
    // 4
    try player.start(atTime: CHHapticTimeImmediate)
    // 5
    hapticEngine.notifyWhenPlayersFinished { _ in
      return .stopEngine
    }
  } catch {
    print("Failed to play slice: \(error)")
  }
}

此代码执行以下操作:

  • 1) 顾名思义,您可以调用slicePattern()来获取您的触觉切片模式(haptic slice pattern)
  • 2) 然后,您在触觉引擎上调用start()
  • 3) 用您的切片模式制作一个模式玩家。
  • 4) 接下来,播放模式,并使用CHHapticTimeImmediate调用start(atTime :)立即播放它。
  • 5) 调用notifyWhenPlayersFinished(finishedHandler :)并返回.stopEngine以在引擎完成播放后停止引擎。

好的,您快到了,但是您需要对GameScene.swift进行一些更新。 首先打开文件,并在类顶部添加以下属性:

private var hapticManager: HapticManager?

然后将以下内容添加到didMove(to :)的顶部:

hapticManager = HapticManager()

checkIfVineCut(withBody :)中,将此行添加到运行切片声音操作的行上方:

hapticManager?.playSlice()

建立并运行并切成薄片的藤蔓! 您能感觉到游戏玩法已经改善吗?


Exploring the Events That Make up the Pattern

好的,一次就很多了。 现在,花点时间深入了解您在这里所做的事情。

着眼于模式,您可以看到它由事件组成:

let slice = CHHapticEvent(
  eventType: .hapticContinuous,
  parameters: [
    CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.35),      
    CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.25)
  ],
  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)

CHHapticEvent表示的触觉事件可以是以下两种类型之一:.hapticTransient.hapticContinuous。瞬态事件是瞬时的,就像是一个鼓声,而连续事件则是一个隆隆声。您还可以创建音频事件,但以后会更多。

每个事件都有两个属性,这些属性控制CHHapticEventParameter表示的触觉。每个都有一个参数ID和一个介于01之间的值。

ID.hapticIntensity的参数表示感觉的强度;值越高,则值越强。 ID.hapticSharpness的参数表示一种物理质量,该质量在比例尺的高端具有精确的机械感。在低端,它具有更圆润的有机感觉。

事件的relativeTime表示从事件发生的模式开始算起的秒数。对于连续事件,还有一个duration属性来定义事件进行的时间。

您的第一个触觉效果是立即开始低0.25秒钟的隆隆声,并在开始时0.08秒发出尖锐而强烈的跳动。您可以在图中表示模式:

接下来,您将学习如何更有效地使用触觉事件。


Managing Energy Usage

Core Haptics在您的iOS设备中使用Taptic Engine硬件。与iOS设备上的所有硬件组件一样,您只需要在必要时将其激活,以免浪费能源。何时运行引擎取决于您的应用程序需求,但以下是一些准则:

  • 如果您的模式立即播放一个瞬态事件,请调用引擎start(),播放该模式,然后调用stop(completionHandler :)。如Apple的触觉模式文档Apple’s haptic pattern documentation中所述,这是最节能的方式。
  • 如果您正在播放更长或更复杂的模式,请像已经完成的那样,调用notifyWhenPlayersFinished(finishedHandler :)并返回.stopEngine
  • 但是,如果您的应用程序像Snip The Vine一样连续播放或重复播放多个模式,则需要引擎始终运行。

现在,您将了解如何确保引擎继续运行。

首先,从playSlice()中删除以下对notifyWhenPlayersFinished(finishedHandler :)的调用:

hapticEngine.notifyWhenPlayersFinished { error in
  return .stopEngine
}

接下来,将其添加到init?()的末尾:

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

这样可以确保在加载游戏场景时触觉引擎已准备就绪。

仍在init?()中,将以下行添加到末尾:

hapticEngine.isAutoShutdownEnabled = true

这实现了触觉引擎的自动关闭。这就是您负责任的能源管理。但是,这意味着iOS可以随时关闭触觉引擎,并且您无法假定它在需要时仍会运行。


Designing a Haptic Experience

您为什么要使用这些特定的事件,属性和时间安排?这次经历对您有何感想?您有没有看到刀片在空中切成薄片和锐利的snip的印象?这些是为用户设计触觉体验时需要询问的问题。

精心设计的触觉体验会以微妙的方式增强应用程序;设计不当的体验会令人烦恼和分散注意力。

精心设计的触觉体验也需要时间。您需要测试许多细微的变化,但是能够与朋友说您刚刚度过了一个下午,为鳄鱼吃菠萝的完美触觉体验,这是一种荣幸。

每个崭露头角的触觉设计师都应阅读Haptics上的Apple人机界面指南页面 Apple Human Interface Guidelines page on HapticsDesigning with Haptics部分提供了大量设计技巧。

何时使用Core Haptics:一般规则是,如果您在应用程序中使用UIKit,则如果使用标准UIKit控件,则可以自由使用Apple设计的Haptics

如果您需要一些不同的东西,则可以使用UIFeedbackGenerator进行通知,影响或选择效果。

如果您确实需要发挥创造力,那么Core Haptics可以满足您的需求。但是,从UIFeedbackGeneratorCore Haptics的复杂性跳跃很大。

现在,该开始研究鳄鱼咬入菠萝时如何获得完美的感觉!


Feeding the Crocodile

那么如何开始设计触觉体验呢?

构建并运行,看看您的鳄鱼,在菠萝掉落时高兴地咀嚼它。

您可以从中获得一些提示:声音和动画。 在Resources/Sounds中查找并在GarageBand或您选择的音频编辑应用中打开NomNom.caf。 您可以看到该声音效果的波形:

波形具有两个明显的高点:初始*crunch*,然后结束时较小的*munch*声。

打开Haptics.swift并在slicePattern()之后添加一个新方法:

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)

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

这是鳄鱼*nom-nom*模式的开始-两个连续的事件,分别对应于nom nom声音效果中的两种声音。

1. Playing Different Patterns

在播放新的模式之前,首先编写一种通用的模式播放方法,以避免代码重复。 在Haptics.swift中,在playHapticSlice()上方添加以下方法:

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

此方法将播放传递给它的任何模式。 它仍然必须尝试启动触觉引擎,因为iOS可能会随时关闭引擎。 因此,您现在可以简化playSlice()

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

您还可以在playSlice()下添加一种方法来播放新效果:

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

GameScene.swift中,找到运行nomNomSoundActiondidBegin(_ :),并在其上方添加以下行:

hapticManager?.playNomNom()

构建并运行。 当然,您将需要足够的技能来喂养鳄鱼以测试您的新效果。

查看GameScene.swift中的runNomNomAnimation(withDelay :)。 当鳄鱼抓到他的款待时,它的值为0.15。 动画运行如下:

  • 1) 闭嘴
  • 2) 等待0.15秒。
  • 3) 张开嘴。
  • 4) 等待0.15秒。
  • 5) 闭嘴

加上几个强劲的节拍来配合那些咬紧牙关的动作,真是太好了。 为此,向nomNomPattern()添加两个以上的事件。 将行return try CHHapticPattern...替换为:

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)

return try CHHapticPattern(
  events: [rumble1, rumble2, crunch1, crunch2],
  parameters: [])

如前所述,瞬态事件是瞬时的,就像单个鼓拍一样。 .hapticIntensity代表感觉的强度(strength of the sensation).hapticSharpness代表physical quality。 更高的值会产生更强大,更突出的触觉事件。 可以根据自己的喜好随意调整数字。 在这里,您添加了两个瞬态事件以匹配这些声音。

构建并运行。 漂亮*snappy*,对不对? 您想要给用户带来愉悦的感觉,因为这是游戏屏幕的成功结果。 令人满意的*snap* *snap*是完美的:


Syncing Audio Events

到目前为止,您已经将音频和触觉分开,但是Core Haptics还支持触觉模式的音频事件。 使用此功能之前,您必须向触觉引擎注册每个音频资源,这将返回一个资源ID,您可以使用该ID来标识用于事件的音频波形。

将以下属性添加到HapticManager

var sliceAudio: CHHapticAudioResourceID?
var nomNomAudio: CHHapticAudioResourceID?
var splashAudio: CHHapticAudioResourceID?

这些保存您的音频资源ID。

接下来,在init?()之后添加以下方法:

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

setupResources()检查每个音频文件的路径。 然后,它使用registerAudioResource(_:options :)注册它们,返回资源ID

如果找不到该文件,该属性将保持为nil,您可以在模式方法中进行检查。 您尚未开始splash模式,稍后再说。

现在,您需要在init?()的末尾添加对setupResources()的调用:

setupResources()

将音频事件添加到nomNomPattern()中的模式。 将return try CHHapticPattern ...替换为:

var events = [rumble1, rumble2, crunch1, crunch2]

// 1
if let audioResourceID = nomNomAudio {
  // 2
  let audio = CHHapticEvent(
    audioResourceID: audioResourceID, 
    parameters: [], 
    relativeTime: 0)
  events.append(audio)
}

// 3
return try CHHapticPattern(events: events, parameters: [])
  • 1) 首先,检查资源ID是否为nil。 如果检查失败,您将仅使用触觉事件。 那就是你的fallback
  • 2) 您可以使用CHHapticEvent中的特殊初始化程序为音频事件创建音频事件,并传入资源ID
  • 3) 最后,您返回模式。

在那里,将音频添加到切片模式。 将slicePattern()中的return try CHHapticPattern ...替换为:

var events = [slice, snip]

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

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

这与您在nomNomPattern()中所做的非常相似。 您检查用于切片操作的资源ID是否不为nil,以便使用该资源ID创建音频事件。

由于您现在将音频包含在触觉模式中,因此游戏场景无需播放该音频。 打开GameScene.swift并找到setUpAudio()

在该方法的最后,您已经设置了sliceSoundAction,splashSoundActionnomNomSoundAction属性:

sliceSoundAction = .playSoundFileNamed(...)
splashSoundAction = .playSoundFileNamed(...)
nomNomSoundAction = .playSoundFileNamed(...)

您需要进行更改,以便游戏场景播放触觉模式而不是音频,但前提是触觉管理器成功注册了这些音频资源ID并可以播放它们。

setUpAudio()中上面的代码替换为以下代码:

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)

该代码首先确保hapticManager不为nil。 如果是这样,它将正常创建声音动作。 这是第一个fallback位置。

如果hapticManager不为nil,它将调用setupHaptics,您现在将其添加到setUpAudio()下:

private func setupHaptics(_ manager: HapticManager) {
}

您将使用setupHaptics(_ :)创建可播放触觉模式的SKAction对象,但是如果触觉音频资源IDnil,则还需要进行回退。 在这种情况下,您可以创建一个SKActiongroup,该组将一起播放声音并运行没有音频的触觉模式。

将以下内容添加到setupHaptics(_ :)

// 1
let sliceHaptics = SKAction.run {
  manager.playSlice()
}
if manager.sliceAudio != nil {
  // 2
  sliceSoundAction = sliceHaptics
} else {
  // 3
  sliceSoundAction = .group([
    .playSoundFileNamed(SoundFile.slice, waitForCompletion: false),
    sliceHaptics
  ])
}
  • 1) 首先,创建触觉动作。 这是调用playSlice()的简单run操作。
  • 2) 如果sliceAudio不为nil,则将此操作分配给sliceSoundAction
  • 3) 但是,如果sliceAudionil,则创建带有两个子动作的组group动作。 第一个是playSoundFileNamed动作,第二个是sliceHaptics动作。

现在,为nomNomSoundAction添加相同的方法:

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

除了使用nomNomHaptics之外,这与sliceSoundAction非常相似。

现在,为splashSoundAction添加一个简单的playSoundFileNamed动作:

splashSoundAction = .playSoundFileNamed(
  SoundFile.splash,
  waitForCompletion: false)

您尚未设计那种触觉体验; 这样可以避免在运行游戏且splashSoundActionnil时发生崩溃的情况。

构建并运行! 现在,Core Haptics播放您的片段和nom-nom音频。


Setting a Reset Handler

现在,您正在使用触觉音频资源,您需要考虑一个新问题。 如果设备上的触觉服务器从故障中恢复,则您的触觉引擎实例将重置。 发生这种情况时,引擎将停止并丢失所有音频资源ID引用。 为防止这种情况,您需要一个重置处理程序。

添加重置处理程序很容易。 首先,将此新方法添加到HapticManager中:

func handleEngineReset() {
  do {
    // 1
    try hapticEngine.start()
    // 2
    setupResources()
  } catch {
    print("Failed to restart the engine: \(error)")
  }
}
  • 1) Apple建议您首先尝试启动引擎。
  • 2) 如果可行,您可以恢复以前注册的所有音频资源ID。

接下来,将以下内容添加到init?()中,以在引擎重置时调用handleEngineReset()

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

有关更多信息,请参阅Preparing Your App to Play Haptics中的Apple文档。

下一步,当鳄鱼错过菠萝时,您将添加触觉。


Ramping Intensity Up and Down — Pineapple Splashdown

Splash.caf声音效果时,会有一个* splish *沉重的声音,然后是* splash *较长的拖尾。 将新方法添加到HapticManager来制作代表声音体验的模式:

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)
    ],
    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: [])
}

您的新触觉体验在* splish *的开始处有一个强烈但全面的瞬态事件,然后是一个更长,更柔和的连续事件,始于0.1秒,* splash *持续了0.6秒:

在播放之前,您需要在playNomNom()下向HapticManager添加一个新方法:

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

返回GameScene.swift中的setupHaptics(_ :),删除临时的splashSoundAction代码,然后添加以下代码来设置splashSoundAction

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

构建并运行和测试它。 * splish *效果很好,但是* splash *只是一个很长的隆隆声; 它太一维了。 它应该更像是波涛汹涌。 幸运的是,有一些事件属性可以为您提供帮助。 使用三个新属性更新启动事件:

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)
  • .attackTime是一个属性,用于控制事件从事件开始时的0开始达到指定强度值所需的秒数。 将其视为加速时间。
  • .decayTime相反,表示强度降低到0所需的时间。
  • .releaseTime控制何时开始衰减衰减。

构建并运行,体验错过鳄鱼和溅入海洋的令人失望的失望。 你能感觉到波浪吗? 在声音播放完毕之前,它应该降低到0强度。


Controlling Intensity With a Parameter Curve

由于您通过这些触觉体验而变得富有创意,为什么不改善片段触觉模式以获得更令人满意的* SsssNIP *感觉? 触觉模式也可以接受适用于整个模式的参数。

首先,像这样更新slice事件属性:

let slice = CHHapticEvent(
  eventType: .hapticContinuous, 
  parameters: [
    CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6),
    CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8)
  ], 
  relativeTime: 0, 
  duration: 0.5)

这增加了强度,清晰度和持续时间。

接下来,在slicePattern()中的两个slicesnip事件之后,创建一种新型的参数CHHapticParameterCurve

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)

参数曲线类似于动画曲线,控制点类似于动画关键帧。

此参数曲线具有ID .hapticIntensityControl,它用作模式中所有事件强度值的乘数。 因为是曲线,所以参数会随着模式播放在控制点之间平滑插值。

例如,第一个控制点在时间0处的值为0.2,这意味着它在开始时将所有事件强度值乘以0.2。 到0.08秒时,它将平滑地增加到1.0的倍数。 到0.24秒,它将平滑地回落到0.2,依此类推。

外观如下:

要使用参数曲线,您需要使用CHHapticPattern(events:parameterCurves :)初始化图案对象。

仍在Haptics.swift中,用以下内容替换slicePattern()中的return语句:

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

这将使用您指定的曲线创建触觉模式。

进行构建并运行,以体验新的动态触觉体验。


Updating Pattern Parameters in Real Time

如果您认为动态参数曲线很棒,请等到看到Core Haptics的高手:CHHapticAdvancedPatternPlayer。 这是一个模式播放器,您可以在播放时对其进行控制。

您的游戏中缺少一些重要的东西。 当玩家在屏幕上挥动手指时,您可以看到粒子效果,但是刀片在空中划片的感觉在哪里? 使用CHHapticAdvancedPatternPlayer,您甚至可以实时控制强度,从而使玩家的手指移动得越快,强度就越强。

首先,向HapticManager添加属性以保存对新player的引用:

var swishPlayer: CHHapticAdvancedPatternPlayer?

接下来,添加一种创建player的方法:

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

这是一种简单的模式:一个持续时间很长的连续事件。 您可以通过调用makeAdvancedPlayer(with :)创建player

接下来,在HapticManager中,将以下行添加到setupResources()中:

createSwishPlayer()

这样,无论何时在初始化程序和触觉引擎重置处理程序中调用setupResources()时,都可以创建swish播放器。 引擎重置时,播放器参考也会重置。

接下来,您需要添加一种启动播放器的方法。 在HapticManager的末尾添加以下内容:

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

startSwishPlayer()首先调用hapticEngine.start(),以防引擎停止。 然后,它使用CHHapticTimeImmediate调用模式播放器的start(atTime :),以便播放器立即启动。

您还需要添加一种方法来停止播放器。 还要在HapticManager的末尾添加此代码:

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

在这里,您尝试通过传递CHHapticTimeImmediate来尽快(几乎立即)停止模式播放器。

返回GameScene.swift,找到touchesBegan(_:with :)并添加以下行以在玩家开始扫过时启动模式player

hapticManager?.startSwishPlayer()

接下来,找到touchesEnded(_:with :)并添加以下行以在播放器的滑动结束时停止模式播放器:

hapticManager?.stopSwishPlayer()

构建并运行,在屏幕上移动手指时,您应该体验player的启动和停止。

现在,该添加魔法了!

1. Making the Player Dynamic

接下来,您将根据用户的动作来调整摆幅的强度。 将以下方法添加到HapticManager

// 1
func updateSwishPlayer(intensity: Float) {
  // 2
  let intensity = CHHapticDynamicParameter(
    parameterID: .hapticIntensityControl, 
    value: intensity, 
    relativeTime: 0)
  do {
    // 3
    try swishPlayer?.sendParameters([intensity], atTime: CHHapticTimeImmediate)
  } catch let error {
    print("Swish player dynamic update error: \(error)")
  }
}
  • 1) 新的updateSwishPlayer(intensity :)接受一个float参数:一个介于01之间的值。
  • 2) 使用该值创建ID.hapticIntensityControlCHHapticDynamicParameter。 此参数的功能与您创建的上一个参数曲线非常相似,可以用作模式中所有事件强度值的乘数。 与曲线不同,这是一次更改。
  • 3) 将动态参数发送给player,使其可以立即应用于正在播放的模式。

返回GameScene.swift并将以下内容添加到touchesMoved(_:with :)

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)

每次系统调用touchesMoved(_:with :)时,您都会更新动态播放器的强度控制值。 您可以使用一种简单的算法来计算强度:从上一次触摸移开的次数越多,强度值就越高。

构建并运行。 现在,剪断藤蔓感觉就像您是一名挥舞着轻剑的绝地骑士!

如果您相信这一点,那么您仅涉及Core Haptics可以实现的目标。 还有更多值得探索的地方。

观看WWDC 2019的Apple会议:

浏览Core Haptics documentation。 也有一些示例Xcode项目可供下载。

不要忘记Haptics上的Apple Human Interface Guidelines以及Designing with Haptics中的技巧。

您可能还需要阅读有关Apple Haptic和Audio Pattern(AHAP)文件格式的信息Apple Haptic and Audio Pattern (AHAP) file format

后记

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269