11
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

UnityAdvent Calendar 2024

Day 15

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

Last updated at Posted at 2024-12-15

この記事は Unity Advent Calendar 2024 の15日目の記事です。

はじめに

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

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

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

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

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

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

light_switch.GIF

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

picker_view.GIF

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

成功時 失敗時
face_success.GIF face_failed.GIF

ゲームだと

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

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

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

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

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

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

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

iPhone でハプティックパターンを再生するには UIFeedbackGenerator 若しくは Core Haptics と言う API を用いることで再生することができます。

UIFeedbackGenerator について

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

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

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

用途について

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

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

Core Haptics について

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

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

Core Haptics の解説については様々な要素が登場する都合から、この記事中に含めると長くなってしまうので、以下の別記事に分けて解説していきます。
(現在執筆中...書き終わり次第にリンクを張って更新します)

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

紹介した API は iOS ネイティブの機能となるので Unity から呼び出すにはネイティブプラグインを実装する必要がありますが、UIFeedbackGenerator 及び Core Haptics に関しては Apple 公式から呼び出すためのプラグインが公開されてます。
( Haptic Feedback については Apple.CoreHaptics を参照)

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

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

UIFeedbackGenerator の解説

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

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

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

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

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

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

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

  • Unity 2022.3.54f1
  • Xcode 16.1

sample.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

サンプルの動作要件としては 「iOS 13 以降」 且つ 「iPhone 6s 以降 (正確に言うと Taptic Engine 搭載機種)」 である必要があります。
(そのため、シミュレーターは勿論、Taptic Engine が搭載されていない iPad 各種でも動作しないものとなります) 3

再生までの流れ

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

UIFeedbackGenerator とサブクラスについて

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

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

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

UINotificationFeedbackGenerator

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

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

notification.png

利用例

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

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

notification_sample.GIF

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

UISelectionFeedbackGenerator

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

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

selection.png

利用例

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

picker_view.GIF

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

selection_sample.GIF

UIImpactFeedbackGenerator

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

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

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

impact.png

利用例

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

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

iOS 標準の場合

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

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

language.GIF

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

long_tap.GIF

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

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

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

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

impact_sample.png

実装解説

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

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

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

ネイティブ側では UINotificationFeedbackGenerator をラップするような形で以下 4つの関数を P/Invoke で呼び出せるように実装します。

ちなみに iOS 17.5 以降からは従来のイニシャライザが非推奨となり、新たにイニシャライザで UIView を要求するように変更されているみたいでした。

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

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

// 生成
@_cdecl("createUINotificationFeedbackGenerator")
func createUINotificationFeedbackGenerator() -> 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("releaseUINotificationFeedbackGenerator")
func releaseUINotificationFeedbackGenerator(_ instancePtr: UnsafeRawPointer) {
    let unmanaged = Unmanaged<UINotificationFeedbackGenerator>.fromOpaque(instancePtr)
    unmanaged.release()
}

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

// 再生
@_cdecl("notificationOccurredUINotificationFeedbackGenerator")
func notificationOccurredUINotificationFeedbackGenerator(_ 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)
}

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

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

Unity 側の実装 (C#)

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

UINotificationFeedbackGenerator.cs
using System;
using System.Runtime.InteropServices;

namespace Examples.UIFeedbackGenerator.Plugins.iOS
{
    /// <summary>
    /// `UINotificationFeedbackGenerator` を C# から扱えるようにしたクラス
    /// </summary>
    /// <remarks>
    /// https://developer.apple.com/documentation/uikit/uinotificationfeedbackgenerator
    /// </remarks>
    public sealed class UINotificationFeedbackGenerator : IDisposable
    {
        // ネイティブ側と数値は合わせておくこと
        // https://developer.apple.com/documentation/uikit/uinotificationfeedbackgenerator/feedbacktype
        public enum FeedbackType
        {
            Success = 0,
            Warning = 1,
            Error = 2,
        }

        private readonly IntPtr _instance;

        public UINotificationFeedbackGenerator()
        {
            _instance = NativeMethod();

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

        public void Dispose()
        {
            NativeMethod(_instance);

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

        public void Prepare()
        {
            NativeMethod(_instance);

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

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

            [DllImport("__Internal", EntryPoint = "notificationOccurredUINotificationFeedbackGenerator")]
            static extern void NativeMethod(IntPtr instance, FeedbackType feedbackType);
        }
    }
}
呼び出し例
using var notification = new UINotificationFeedbackGenerator();
notification.Prepare();
notification.NotificationOccurred(UINotificationFeedbackGenerator.FeedbackType.Success);

おわりに

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

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

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

参考リンク

  1. 円形に並んだパックをぐるぐる回して選ぶ画面

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

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

  4. これ以外にも UICanvasFeedbackGenerator と言う派生クラスも存在しますが、こちらは Apple Pecil Pro 用?の機能と思われ、実機検証含めて調査できていないので今回の解説からは割愛します。

  5. SwiftUI で言うところの WheelPickerStyle 的なもの

  6. スクロール時のスナップなどの機能が欲しかったので、実装するにあたっては LightScrollSnap を使わせてもらいました

  7. 実際に UIImpactFeedbackGenerator で再生されているかまでは不明ですが... 2

11
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?