【Unity】iPhone アプリでの Haptic Feedback 入門

この記事は [Unity Advent Calendar 2024](https://qiita.com/advent-calendar/2024/unity) の15日目の記事です。

# はじめに

iPhone には、画面の様々なアクションに応じて振動を発生させる **Haptic Feedback (触覚フィードバック)** と言う仕組みが備わっています。

こちらをゲームやアプリで導入すれば「UI を操作した際に発生させる軽い振動」や「ゲームの状況に応じた振動 (例えばアクションゲームの場合には攻撃時の衝撃感)」など、**「視覚や音だけでは伝わらない物理的な感覚」をユーザーに届けることが可能になるかと思います。**

この記事では Untiy プロジェクトを前提として概要や導入方法、実装例などについて解説していきます。

## 実際にどこで使われているのか？

例えば iOS の場合には OS 標準で様々なところに組み込まれており、幾つか例を挙げると次のようなところで振動(パプティックパターン)が再生されます。

**ロック画面にあるフラッシュライトの ON / OFF**

![light_switch.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/e2fc0887-25ed-3865-6bb8-7c1d16f6d5c5.gif)

**カレンダーアプリでの時間選択時のスクロール**

![picker_view.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/889cdc55-6241-9382-9a67-3fc992e53c54.gif)

**Face ID による認証処理の結果表示**

| 成功時 | 失敗時 |
|:--:|:--:|
| ![face_success.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/2114026d-0a4a-5107-7bfb-6db58b4c074d.gif) | ![face_failed.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/f133067f-5a58-af19-5876-d756357dfe41.gif) |


### ゲームだと

自分はそこまで多くのゲームをやらないので実際にどれくらい導入事例があるのかは不明ですが...ここ最近で観測したものだと以下のアプリで導入されているように思われました。
(※あくまで多分使われてるかも？レベルの予想です。実際に内部的に使われているかは不明)

- **ポケポケ (Pokémon Trading Card Game Pocket)**
    - 開封したいパックを選択しているとき[^6]のフィードバック
    - パックを開封するときのフィードバック
    - 入手したカードが「自分のカード」に加わる際のフィードバック
- **キャンディクラッシュ**
    - ブロックが消滅する時のフィードバックなど様々
- **Pikmin Bloom**
    - ピクミンを引っこ抜く際のフィードバック

[^6]: 円形に並んだパックをぐるぐる回して選ぶ画面

どれもアプリ内でのアクションに対し、物理的なフィードバックを加えることで使用感を上げると言った意図があるのかなと考えられます。


## ベストプラクティスについて

Apple の `Human Interface Guidelines` (以降、`HIG` と省略) には **Haptic Feedback を実装する上でのガイドラインとしてのベストプラクティスが記載されています。**

https://developer.apple.com/jp/design/human-interface-guidelines/playing-haptics

この章では引用しての解説は割愛しますが、心構えとしては重要になってくるかと思われるので、**機能を利用する前に一読してみることをおすすめします。**


# 振動(ハプティックパターン)を再生するには？

iPhone でハプティックパターンを再生するには **[UIFeedbackGenerator](https://developer.apple.com/documentation/uikit/uifeedbackgenerator)** 若しくは **[Core Haptics](https://developer.apple.com/documentation/corehaptics)** と言う API を用いることで再生することができます。

https://developer.apple.com/documentation/uikit/uifeedbackgenerator

https://developer.apple.com/documentation/corehaptics


## `UIFeedbackGenerator` について

`UIFeedbackGenerator` は **「システムが提供するプリセットのハプティックパターン」** を再生するための API であり、再生可能なものとしては「**アクションやイベントに対する結果(成功, 失敗など)を示すためのパターン**」や 「**UI の選択状態の変更などを示すためのパターン**」などがあります。

あとは API がシンプルであり、ネイティブ側だけで言うと数行程度で呼び出すことが可能なので、**用途さえ理解しておけば比較的手軽に導入可能な点についても魅力的です。**

こちらについての具体的な解説は別章にて後述します。

### 用途について

名前に `UI` と付いてある通り、UI 周りでの利用が主になるかと思われるので、ゲーム系統での利用を想定するのであればアウトゲーム周りやインゲーム中の UI とかでは使えるかもしれません。[^7]

一方でプリセット以外のパターンは再生することが出来ないので、もし **「インゲーム周りで独自のパターンを作って再生したい」** と言ったケースがある場合には、次で解説する `Core Haptics` を使う必要があります。

[^7]: 一応 `UIImpactFeedbackGenerator` (詳細は後述) 辺りであればインゲーム周りでも使い所によっては使えるかもしれない？ただ、無理に組み込んで適さないパターンを再生するのはベストプラクティスにも反するので、オリジナリティの高いものは次で解説する `Core Haptics` を用いるのが良いかもしれません。

## `Core Haptics` について

もう一方の `Core Haptics` は **「独自定義した任意のパプティックパターンを再生することが可能な API」**　であり、自分で定義したパターン再生の他にも「再生中のパターンの動的な値の変更」や「オーディオ機能との連携」が可能だったりと、**かなりカスタマイズ性の高い API となってます。**

そのため、ゲーム系統のアプリでは上手く使いこなすことができれば没入感のある体験を提供できるようになるかと思います。

`Core Haptics` の解説については様々な要素が登場する都合から、この記事中に含めると長くなってしまうので、別記事に分けて解説していきます。

https://qiita.com/mao_/items/c81bdff01e5f987bff51


## Apple 公式の Unity プラグインを利用することが可能

紹介した API は iOS ネイティブの機能となるので Unity から呼び出すには[ネイティブプラグイン](https://docs.unity3d.com/Manual/plug-ins-native.html)を実装する必要がありますが、**`UIFeedbackGenerator` 及び `Core Haptics` に関しては Apple 公式から呼び出すためのプラグインが公開されてます。**
( Haptic Feedback については [Apple.CoreHaptics](https://github.com/apple/unityplugins/blob/main/plug-ins/Apple.CoreHaptics/Apple.CoreHaptics_Unity/Assets/Apple.CoreHaptics/Documentation~/Apple.CoreHaptics.md) を参照)

https://github.com/apple/unityplugins

https://developer.apple.com/jp/videos/play/wwdc2022/10065/

:::note warn
**ちなみに、この記事では内部実装についても一部補足を交えつつ解説すると言った目的もあるので、基本的には上記プラグインは使わずに自前で再実装していく形で解説していきます。**

ただ、基本的な実装方針としてはお互いネイティブの API に寄せる形で作っているので、ここで把握した内容を理解しておけば知見はそのまま使い回せるかと思います。
:::




# `UIFeedbackGenerator` の解説

ここからは `UIFeedbackGenerator` について具体的な解説に入っていきます。

最初に簡単な概要説明や「どういった用途で使えるのか？」と言った利用例などの解説から入り、その後に実装について触れていきます。

https://developer.apple.com/documentation/uikit/uifeedbackgenerator

:::note info
先に断っておくと、この章で解説する利用例などは `HIG` に目を通した上での自分なりの解釈などを交えたものとなります。 
なので、必ずしもこれが正解といったものでは無いかもしれないので、**あくまで実装の一例として見て頂けると幸いです。**

その上でもし「その説明は違うのでは？」と言った違和感などありましたら、コメントなどでご指摘頂けると助かります :bow: 
:::

## サンプルプロジェクトについて

解説用にサンプルプロジェクトを用意してます。

取り扱っている内容が **Haptic Feedback と言う物理的に作用するもの**となっているので、是非とも**実機ビルドをした上で実際に体験しながら読み進めて頂けると幸いです。**

https://github.com/mao-test-h/Advent-Calendar-2024-SampleProject


- Unity 2022.3.54f1
- Xcode 16.1

![sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/0b68ef3f-1eeb-a356-cced-3dc398d81055.png)

シーン及びコード一式は以下のフォルダをご覧ください。

```
Assets/Examples/UIFeedbackGenerator
.
├── Textures
│   └── (サンプルで用いているテクスチャ)
├── Plugins
│   └── iOS
│       ├── UIImpactFeedbackGenerator.cs
│       ├── UIImpactFeedbackGeneratorBridge.swift
│       ├── UINotificationFeedbackGenerator.cs
│       ├── UINotificationFeedbackGeneratorBridge.swift
│       ├── UISelectionFeedbackGenerator.cs
│       └── UISelectionFeedbackGeneratorBridge.swift
├── UIFeedbackGenerator.asmdef
├── UIFeedbackGeneratorExample.cs
└── UIFeedbackGeneratorExample.unity
```

:::note warn
サンプルの動作要件としては **「iOS 13 以降」** 且つ **「iPhone 6s 以降 (正確に言うと [Taptic Engine](https://en.wikipedia.org/wiki/IPhone_hardware#Taptic_Engine) 搭載機種)」** である必要があります。
(そのため、シミュレーターは勿論、Taptic Engine が搭載されていない iPad 各種でも動作しないものとなります) [^1]
:::

[^1]: ちなみに DualSense と言ったゲームコントローラーに接続すれば、コントローラー側で再生可能みたいですが...この記事では未調査なため取り扱いません。


## 再生までの流れ

ハプティックパターンを再生するには `UIFeedbackGenerator` のサブクラスをネイティブプラグインを用いてインスタンス化し、その中にある再生メソッドを呼び出す必要があります。

### `UIFeedbackGenerator` とサブクラスについて

`UIFeedbackGenerator` 自体は `prepare` と言うメソッドだけを持つ基底クラスであり、用途に応じた次のサブクラスが用意されています。[^2]  

[^2]: これ以外にも [UICanvasFeedbackGenerator](https://developer.apple.com/documentation/uikit/uicanvasfeedbackgenerator) と言う派生クラスも存在しますが、こちらは Apple Pecil Pro 用？の機能と思われ、実機検証含めて調査できていないので今回の解説からは割愛します。

- **`UINotificationFeedbackGenerator`**
    - アクションやイベントに対する「成功 / 警告 / 失敗」を示すためのパターン
- **`UISelectionFeedbackGenerator`**
    - UI の選択状態の変更などを示すためのパターン
- **`UIImpactFeedbackGenerator`**
    - 物理的なメタファーによって視覚情報を補完する際に利用可能なパターン

あとは[ドキュメントにも記載](https://developer.apple.com/documentation/UIKit/UIFeedbackGenerator)されている通り、基底クラスである `UIFeedbackGenerator` から自身でサブクラスを作ったりインスタンス化を行うのは禁止されているので、基本的には公式から提供されている派生クラスのみを用いる流れとなります。


## `UINotificationFeedbackGenerator`

`UINotificationFeedbackGenerator` はアクションやイベントに対する「成功 / 警告 / 失敗」を示すためのパターンを再生する API です。  

https://developer.apple.com/documentation/uikit/uinotificationfeedbackgenerator  

`HIG` からは再生可能なパターンを確認することが可能であり、ここで取り上げている `Notification` の場合には次のようなパターンが用意されています。

![notification.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/251e91b2-5c7a-3fc0-b958-7070be30e5b0.png)

### 利用例

主な利用箇所としてはガイドラインにも記載されているように、一連のタスクやアクションを行った際の成功の可否を伝えるなどが挙げられるかと思います。

例えばサンプルでは以下のように擬似的なアプリ内通知を表示するタイミングでそれぞれのパターンの再生を行っています。

![notification_sample.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/838f639b-c44b-10f9-e420-a17c9ba95138.gif)

他にも一連のトランザクションの成功・失敗を上記のような通知以外で知らせる際にも利用箇所としては適しているかもしれません。



## `UISelectionFeedbackGenerator`

`UISelectionFeedbackGenerator` は UI の選択状態や値の変更などを示すためのパターンを再生する API です。  

https://developer.apple.com/documentation/uikit/uiselectionfeedbackgenerator  

`HIG` から引用すると次のようなパターンが再生可能です。

![selection.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/ee4d549c-a53b-f6e1-123f-a1b12f4fbc63.png)

### 利用例

主な用途としてはドラムロール UI [^4]で選択項目を変更した時のフィードバックなどが挙げられるかと思います。
(イメージ的には記事冒頭でも貼った以下のような UI を Unity 上で実装するケースなど)

![picker_view.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/889cdc55-6241-9382-9a67-3fc992e53c54.gif)

サンプルではスクロールビューにこちらを組み込み、要素が切り替わるタイミングでパターンを再生するようにしています。[^5]

![selection_sample.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/886325d5-4084-4755-298d-4c47da82b032.gif)

[^4]: SwiftUI で言うところの [WheelPickerStyle](https://developer.apple.com/documentation/swiftui/wheelpickerstyle) 的なもの

[^5]: スクロール時のスナップなどの機能が欲しかったので、実装するにあたっては [LightScrollSnap](https://github.com/emrecelik95/LightScrollSnap) を使わせてもらいました



## `UIImpactFeedbackGenerator`

`UIImpactFeedbackGenerator` は一言でいえば **視覚情報を補完する**ために利用可能なパターンが揃っている API になるかと思います。

ドキュメントには例として「UI オブジェクトが何かと衝突したり、所定の位置にスナップした時」に再生できると言った説明がされています。

https://developer.apple.com/documentation/uikit/uiimpactfeedbackgenerator  

こちらは `HIG` から引用すると次の 5パターンが再生可能です。

![impact.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/a5fda7af-53bc-59d6-3a88-39a670cba583.png)


### 利用例

こちらは主に「**UI を操作した際に物理的なフィードバックを発生させる**」といった用途が考えられそうです。

例えば現実世界にある物理的なボタンを押した際には「カチッ」と言った衝撃(触覚)が発生して指に伝わるかと思いますが、イメージ的にはそれを擬似的に発生させると言った形になるかと思われます。


#### iOS 標準の場合

OS 側で使われていそうな箇所を挙げると、例えば以下のようなリストを操作したタイミングなどで `Light` ~ `Medium` 辺りが再生されているように思われました。

これはまさに HIG にも書かれている通り、UI を特定の位置にスナップさせた時に発生するパターンが該当するかと思われます。[^3]

[^3]: 実際に `UIImpactFeedbackGenerator` で再生されているかまでは不明ですが...

![language.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/9803d6de-1bbd-562b-8a28-7e6bc33c2fbb.gif)

他にもそれっぽいところを探した所、ホーム画面でアプリをロングタップするとポップアップが表示されるのですが、このタイミングでは `Heavy` 辺りが再生されているように思われました。[^3]

![long_tap.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/0e6b3e04-e4fc-b240-14f2-0d8e4ca0061a.gif)


#### 物理的な特性を反映させる

`HIG` には「物質的なメタファーによって視覚的な体験を補完したい場合に」と記載されているので、操作対象の物理的な特性によって再生するパターンを選択することも出来そうです。

例えば再生できるパターンの中には `Soft`, `Rigid` と言うのもあるので、「柔らかいものをタップした際には `Soft` を再生」と言った使い方もできるかと思われます。

サンプルではそれに倣って以下のように「柔らかいもの (羊さん) をタップしたら `Soft` を再生」「硬いもの (岩) をタップしたら `Rigid` を再生」するボタンを配置してみてます。

![impact_sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/5b234d28-f9c8-616d-9464-8958f267cc41.png)



## 実装解説

最後に **`UINotificationFeedbackGenerator`** を例に実装周りについて解説します。
`Impact` と `Selection` についての解説は割愛しますが、基本的な実装は似ているので、興味のある方はサンプルコードをご覧ください。

あとはこの記事ではネイティブプラグインの基本的な実装方法までは取り扱わないので、キャッチアップが必要な方は拙著ですが、以下の記事などに目を通してみてください。

https://qiita.com/mao_/items/5b33c90e533a538570b8

https://qiita.com/mao_/items/07466e169d08cbeff221


### ネイティブ側の実装 (Swift)

ネイティブ側では [`UINotificationFeedbackGenerator`](https://developer.apple.com/documentation/uikit/uinotificationfeedbackgenerator) をラップするような形で以下 4つの関数を P/Invoke で呼び出せるように実装します。

- 生成
- 解放
- 準備 ( [`prepare()`](https://developer.apple.com/documentation/uikit/uifeedbackgenerator/prepare()) )
- 再生 ( [`notificationOccurred(_:)`](https://developer.apple.com/documentation/uikit/uinotificationfeedbackgenerator/notificationoccurred(_:)) )

:::note warn
ちなみに iOS 17.5 以降からは[従来のイニシャライザ](https://developer.apple.com/documentation/uikit/uifeedbackgenerator/init())が非推奨となり、[新たにイニシャライザで `UIView` を要求するように変更](https://developer.apple.com/documentation/uikit/uifeedbackgenerator/init(view:))されているみたいでした。

とは言え、今回導入する対象は Unity であり、ネイティブ側のように細かな View 指定は出来ないため、**とりあえずの対処として `UnityFramework` から Unity が内部的に生成している `rootView` を取得してそちらを渡すような形にしています。**
(もしこの実装で怪しい点などありましたら、コメント等で教えて頂けると幸いです :bow: )

```swift
let instance: UINotificationFeedbackGenerator
if #available(iOS 17.5, *),
   let rootView = UnityFramework.getInstance().appController().rootView {
    instance = UINotificationFeedbackGenerator(view: rootView)
}
```
:::


```swift:UINotificationFeedbackGeneratorBridge.swift
import Foundation
import UIKit

// インスタンス化
@_cdecl("init_UINotificationFeedbackGenerator")
func init_UINotificationFeedbackGenerator() -> UnsafeMutableRawPointer {
    let instance: UINotificationFeedbackGenerator
    if #available(iOS 17.5, *),
       let rootView = UnityFramework.getInstance().appController().rootView {
        instance = UINotificationFeedbackGenerator(view: rootView)
    } else {
        instance = UINotificationFeedbackGenerator()
    }
    
    let unmanaged = Unmanaged<UINotificationFeedbackGenerator>.passRetained(instance)
    return unmanaged.toOpaque()
}

// 解放
@_cdecl("release_UINotificationFeedbackGenerator")
func release_UINotificationFeedbackGenerator(_ instancePtr: UnsafeRawPointer) {
    let unmanaged = Unmanaged<UINotificationFeedbackGenerator>.fromOpaque(instancePtr)
    unmanaged.release()
}

// 準備
@_cdecl("prepare_UINotificationFeedbackGenerator")
func prepare_UINotificationFeedbackGenerator(_ instancePtr: UnsafeRawPointer) {
    let instance = Unmanaged<UINotificationFeedbackGenerator>.fromOpaque(instancePtr).takeUnretainedValue()
    instance.prepare()
}

// 再生
@_cdecl("notificationOccurred_UINotificationFeedbackGenerator")
func notificationOccurred_UINotificationFeedbackGenerator(_ instancePtr: UnsafeRawPointer, _ notificationType: Int32) {
    guard let notificationType = UINotificationFeedbackGenerator.FeedbackType(rawValue: Int(notificationType)) else {
        fatalError("invalid type")
    }
    
    let instance = Unmanaged<UINotificationFeedbackGenerator>.fromOpaque(instancePtr).takeUnretainedValue()
    instance.notificationOccurred(notificationType)
}

```

:::note info
今回のサンプルでは未実装ですが、iOS 17.5 からは新たに座標を指定できる API が追加されているみたいでした。( `Impact`, `Selection` も同様)

- [`notificationOccurred(_:at:)`](https://developer.apple.com/documentation/uikit/uinotificationfeedbackgenerator/notificationoccurred(_:at:))

こちらもどっかで検証できたら追記するかもです。
:::

### Unity 側の実装 (C#)

Unity 側の呼び出しは次のようになります。
特に補足する箇所も無いのでコードだけ乗せておきます。

```csharp:UINotificationFeedbackGenerator.cs
using System;
using System.Runtime.InteropServices;

namespace Examples.UIFeedbackGenerator.Plugins.iOS
{
    /// <summary>
    /// <see href="https://developer.apple.com/documentation/uikit/uinotificationfeedbackgenerator">UINotificationFeedbackGenerator</see> の実装
    /// </summary>
    public sealed class UINotificationFeedbackGenerator : IDisposable
    {
        /// <summary>
        /// <see href="https://developer.apple.com/documentation/uikit/uinotificationfeedbackgenerator/feedbacktype">UINotificationFeedbackGenerator.FeedbackType</see>
        /// </summary>
        public enum FeedbackType
        {
            // 数値はネイティブ側と合わせておく必要がある
            Success = 0,
            Warning = 1,
            Error = 2,
        }

        private readonly IntPtr _instance;

        public UINotificationFeedbackGenerator()
        {
            _instance = NativeMethod();

            [DllImport("__Internal", EntryPoint = "init_UINotificationFeedbackGenerator")]
            static extern IntPtr NativeMethod();
        }

        public void Dispose()
        {
            NativeMethod(_instance);

            [DllImport("__Internal", EntryPoint = "release_UINotificationFeedbackGenerator")]
            static extern void NativeMethod(IntPtr instance);
        }

        public void Prepare()
        {
            NativeMethod(_instance);

            [DllImport("__Internal", EntryPoint = "prepare_UINotificationFeedbackGenerator")]
            static extern void NativeMethod(IntPtr instance);
        }

        public void NotificationOccurred(FeedbackType feedbackType)
        {
            NativeMethod(_instance, feedbackType);

            [DllImport("__Internal", EntryPoint = "notificationOccurred_UINotificationFeedbackGenerator")]
            static extern void NativeMethod(IntPtr instance, FeedbackType feedbackType);
        }
    }
}

```

```csharp:呼び出し例
using var notification = new UINotificationFeedbackGenerator();
notification.Prepare();
notification.NotificationOccurred(UINotificationFeedbackGenerator.FeedbackType.Success);
```

# おわりに

iOS の Haptic Feedback について、自分の理解を交えながら一通り解説してみました。

正直 Haptic Feedback については何もわからん状態から調査に入ったのですが、これらは `UIFeedbackGenerator` の章で解説したように用途に応じたパターンが存在することや、更に細かく調整できる API (`Core Haptics`) が存在することを知れたりなど、意外と奥が深かったと言う印象です。

これを機に Haptic Feedback について興味を持てたので、いずれ Android の方についてもキャッチアップできたらな〜と思ってます。


# 参考リンク

- Human Interface Guildelines
    - [触覚フィードバックの提供](https://developer.apple.com/jp/design/human-interface-guidelines/playing-haptics)
- API Reference
    - [UIFeedbackGenerator](https://developer.apple.com/documentation/uikit/uifeedbackgenerator)
- 参考記事
    - [iPhone アプリでの Haptic Feedback による効果的なマイクロインタラクション](https://note.com/tdksk/n/nb4498e59dcad)
    - [UIFeedbackGeneratorの使い方と便利に使えるライブラリ](https://techblog.zozo.com/entry/ios_ui_feedbcak_generator)
    - [[iOS 10] iOS Human Interface GuidelinesのHaptic Feedbackを読んでみる](https://dev.classmethod.jp/articles/ios-10-ui-haptic-feedback/)
    - [【iOS】入力文字数制限に近づいた際にHaptic feedback(触覚フィードバック)でユーザに注意喚起する](https://qiita.com/MilanistaDev/items/9eb407b923096a1de8f5)
    - [Haptic Feedbackでクセになるユーザー体験を提供しよう!](https://speakerdeck.com/cocona15531/haptic-feedbackdekuseninaruyuzati-yan-woti-gong-siyou)