単純なバイブレーションの実装
Unityでは
Handheld.Vibrate ();
というメソッドが用意されていて、iOSでもAndroidでも振動させるだけならこれ一つで実現できます。
が、これは ただ1秒くらい「ブーン」となる以外に何も調整ができず 、UXや演出として利用するには流石に "わびさび" が足りません。
ちょっと凝ったバイブレーションの実装
ちょっとだけ手間ですが、iOS、Androidのネイティブ処理を呼び出すことで少し融通の利くバイブレーションを実装できます。下記の記事が分かりやすくまとまっていて、参考になります!
Androidは端末のバリエーションが多すぎる故か、これで 振動時間を調整するのが限界 ですが、一応Unityに用意されたメソッドを呼ぶよりはマシなバイブレーションが実装できるはずです。
iOSでは、AudioServicesPlaySystemSound
というネイティブのメソッドを使うことで、 OSに用意されたサウンドID を渡し、その中で音無し+バイブレーション有りのパターンのものを使うことができます。
ざっと確認した感じ、使えそうなのは以下のサウンドIDでした。
1003:ブッ(ごく小さな音が入っているが、ほぼ聞こえず)
1011:ブッブー
1102:ププッ
1161:カリッ
1162:カリカリッ
1164:カリカリカリ...
1311:ブッブー
1350:ブーッ
1519:プッ
1521:プルッ
通知やUX用に用意されたパターンの振動なので、それなりに使い勝手の良いバイブレーションが揃っています。 めちゃくちゃこだわるようなことがなければこの中から選べばよい と思います。
が、あくまでパターンの中から選ぶ形なので、振動時間は調整できないし、縛りが強いと言わざるをえません。
しかし、 Haptics を使えばかなり細かい振動を実装できます。
Haptic Feedback(触覚フィードバック)
iPhoneは、iPhone7以降、ホームボタンが物理ボタンではなくなりました。(2020 10/16 追記:嘘でした。バリバリ物理ボタンありましたw)
そこで生まれたのが 「Haptic Feedback」 と呼ばれる、 タッチディスプレイ上でもボタンを押したようなフィードバックを得られるバイブレーション の仕組みです。
iPhone7以降の端末では、画面の下から上にフリックすることでホーム画面へ戻ることができますが、その時に「グッ」というような振動が起きる、あれです。
そして iOS13 以降、この仕組みを自由に使える Core Haptics というAPIが追加されました。
これを使えば、振動時間だけでなく、 振動の「強さ」や「鋭さ」 というような、Haptic Feedback で使われていたかなり細かなバイブレーションのパラメータを調整することができます(この辺はOSも端末も一社で作ってるAppleにしかできない仕組みですね)。
生まれた経緯ゆえ、 かなりUXの強化に向いた機能 になっています!
ということで、本記事ではそれをUnityから使うためのアレコレをまとめます。
ネイティブ実装は普段頻繁にやるようなものでもないし、Objective-C や Swift を書くことになるので、思いのほか手こずりました(というか徹夜するハメになりました)。
この記事でこれから Haptics を使う人が徹夜しなくて済むようになりますように。。。
■余談 - Macbook の Haptics
最近のMacbookではトラックパッドを押したときに「カチッ」という感触がありますが、実はあれも Haptic Feedback と同じ仕組みを使ってるらしいです。実際には押し込めるような構造になってないので電源を消してトラックパッドを押しても何にも感触がありません。(自分もHaptics を調べてるときに知ってクッソ驚きました)
検証環境
本記事は以下のバージョンで動作を確認してます。
- Unity 2018.4.11f1
- Xcode 11.1
ファイルの作成
Core Haptics はiOSネイティブで提供されているAPIで、Objective-C でも Swift でも呼び出せるようになっていますが、今回は Swift での実装方法 を紹介します。
上図のように、Unityから Swift の処理を呼ぶには iOSプラグインを経由する必要があります。
以下のフォルダ構造で、それぞれファイルを作成してください。
Assets
┣ VibrationUtil.cs
┗ Plugins
┣ VibrationObjc.mm
┗ iOS
┗ VibrationSwift.swift
■余談 - なんでSwift?
Objective-C で処理を書けば、iOSプラグインファイルで直接 Core Haptics API が呼べます。
普段 Objective-C はおろか、Swift も書かない僕はその方法で済ませたかったですが、公式ドキュメントの実装例 が なぜか Swiftの分しか無かったので、今回は泣く泣くSwiftを選ぶことにしました。
SwiftでCore Hapticsの処理を記述
ファイル全文
import Foundation
import CoreHaptics
@objc public class VibrationSwift : NSObject {
@available(iOS 13.0, *)
@objc public static var hapticEngine: CHHapticEngine?
@available(iOS 13.0, *)
@objc public static func setupHapticEngine() {
do {
hapticEngine = try CHHapticEngine()
try hapticEngine?.start()
} catch {
print("Failed to restart the engine")
}
}
@available(iOS 13.0, *)
@objc public static func playHapticEngine(intensityValue: Float, sharpnessValue: Float, durationValue: Float, sustainedValue: Float) {
if hapticEngine == nil {
setupHapticEngine()
}
let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: intensityValue)
let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: sharpnessValue)
let sustained = CHHapticEventParameter(parameterID: .sustained, value: sustainedValue)
let event = CHHapticEvent(eventType: .hapticContinuous, parameters: [intensity, sharpness, sustained], relativeTime: 0, duration: TimeInterval(durationValue))
do {
let hapticPattern = try CHHapticPattern(events: [event], parameters: [])
let hapticPlayer = try hapticEngine?.makePlayer(with: hapticPattern)
try hapticPlayer?.start(atTime: 0)
} catch {
print("Failed to play")
}
}
}
解説
import CoreHaptics
まず、必ず CoreHaptics をインポートしてください。
@objc public static var hapticEngine: CHHapticEngine?
CoreHaptics による振動を管理する CHHapticEngine
の変数です。static にして、static メソッドから参照できるようにしてください。
@objc public static func setupHapticEngine()
変数定義した hapticEngine
の初期化処理です。
@objc public static func playHapticEngine(intensityValue: Float, sharpnessValue: Float, durationValue: Float, sustainedValue: Float)
Hapticsによる振動メソッドです。引数はそれぞれ以下のパラメータを表しています。
- intensityValue:振動の強さ(0~1)
- sharpnessValue:振動の鋭さ(0~1)
- durationValue:振動する時間(秒)
-
sustainedValue:指定した時間の間、振動させ続けるか否か(0ならfalse、1ならtrue)
-
durationValue
の時間は、これがtrueでないと無効となります
-
メソッド内の CHHapticEventParameter
を使えば、他にもいろんなパラメータを扱えるようですが、ひとまず主要なものはこんなところかなと。
■参考 - その他のパラメータ
- attackTime:触覚パターンの強度が増加し始める時間
- decayTime:触覚パターンの強度が減少し始める時間
- releaseTime:触覚パターンのフェードを開始する時間
※ ドキュメント の「Haptic Event Parameter IDs」を参照
let attackTime= CHHapticEventParameter(parameterID: .attackTime, value: 0)
というようにパラメータを定義し、CHHapticEvent
の parameters
に渡すことで設定できます。
上記コードを例にすると、
let event = CHHapticEvent(eventType: .hapticContinuous, parameters: [intensity, sharpness, sustained, attackTime], relativeTime: 0, duration: TimeInterval(durationValue))
こんな感じです。(sustained
の後に attackTime
を追加)
■参考 - 遅延実行
CHHapticEvent
の relativeTime
の引数を使うことで、遅延実行をすることもできます。
例えば「ブブッ」というような複数回の振動パターンを作るときに必要になるでしょう。
let event = CHHapticEvent(eventType: .hapticContinuous, parameters: [intensity, sharpness, sustained], relativeTime: TimeInterval(1), duration: TimeInterval(durationValue))
こんな感じで、TimeInterval
をかまして時間(秒)を指定してください。
@available(iOS 13.0, *)
各変数/メソッドの前に書かれているこの記述ですが、これは Core Haptics が iOS13 から導入されたものであるため、それ以前のバージョンのOSからは呼ばないでね、というやつです。無いとエラーが出ます。
Swiftの処理をiOSプラグインで呼び出し
ファイル全文
#import <Foundation/Foundation.h>
#import <CoreHaptics/CoreHaptics.h>
#import <UnityFramework-Swift.h>
#ifdef __cplusplus
extern "C" {
#endif
void setupHapticEngine() {
[VibrationSwift setupHapticEngine];
}
void playHapticEngine(float intensity, float sharpness, float duration, float sustained) {
[VibrationSwift playHapticEngineWithIntensityValue:intensity sharpnessValue:sharpness durationValue:duration sustainedValue: sustained];
}
#ifdef __cplusplus
}
#endif
解説
#import <UnityFramework-Swift.h>
これは、Xcodeのビルド時に自動的に作られるSwiftのヘッダーファイルです。
何もしないとファイル名は 「{プロジェクト名}-Swift.h」 となるのですが、それだと汎用性に欠けるので、後ほどUnityの実装で任意の名前に固定します。※今回は 「UnityFramework-Swift.h」 としました
[VibrationSwift 〇〇〇];
VibrationSwift.swift
で定義した VibrationSwift
クラスの、「〇〇〇」というメソッドを呼ぶ、という記述です。
[VibrationSwift playHapticEngineWithIntensityValue:intensity sharpnessValue:sharpness durationValue:duration sustainedValue: sustained];
VibrationSwift
クラスの「playHapticEngine」というメソッドを呼んでいる…のですが、ここ、Objective-C の独特な文法です。
第1引数を渡すとき、メソッド名に 「With+イニシャルを大文字にした引数名」 という記述をくっつけて、コロン(:)の後に第1引数書いています。第2引数以降は「引数名:引数」で書いていけばOKです。
Objective-Cを知らないが故、ちょっとハマりました。
iOSプラグインの処理をUnityで呼び出し
ようやくUnityに辿り着いた!
ファイル全文
using System.Runtime.InteropServices;
using UnityEditor.Callbacks;
using System.IO;
#if UNITY_IOS
using UnityEditor.iOS.Xcode;
#endif
public class VibrationUtil {
#if UNITY_IOS && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern void setupHapticEngine();
[DllImport("__Internal")]
private static extern void playHapticEngine(float intensity, float sharpness, float duration, float sustained);
#endif
public static void SetupHapticEngine() {
#if UNITY_IOS && !UNITY_EDITOR
setupHapticEngine();
#endif
}
public static void PlayHapticEngine(float intensity, float sharpness, float duration) {
#if UNITY_IOS && !UNITY_EDITOR
float sustained = duration > 0 ? 1 : 0;
playHapticEngine(intensity, sharpness, duration, sustained);
#endif
}
[PostProcessBuild(0)]
public static void OnPostprocessBuild(BuildTarget target, string path) {
string projectPath = PBXProject.GetPBXProjectPath(path);
PBXProject pbxProject = new PBXProject();
pbxProject.ReadFromString(File.ReadAllText(projectPath));
string target = pbxProject.TargetGuidByName("Unity-iPhone");
pbxProject.SetBuildProperty(target, "SWIFT_VERSION", "4.2");
pbxProject.SetBuildProperty(target, "SWIFT_OBJC_INTERFACE_HEADER_NAME", "UnityFramework-Swift.h");
pbxProject.AddFrameworkToProject(target, "CoreHaptics.framework", false);
File.WriteAllText(projectPath, pbxProject.WriteToString());
}
}
解説
[DllImport("__Internal")]
private static extern void 〇〇〇();
iOSプラグインで定義した「〇〇〇」というメソッドをC#(Unity)に定義する、という記述です。
定義すれば、C#内で普通のメソッドのように扱えます。
[PostProcessBuild(0)]
public static void OnPostprocessBuild(BuildTarget target, string path)
PostProcessBuild
を付けたメソッドは、UnityのiOSビルドでXcodeプロジェクトが出力された後に呼び出されるようになります。(カッコ内の数字はその順番。小さいほど先に呼ばれる)※ドキュメント
これを利用して、Xcodeプロジェクトでやるべき設定を自動化しています。
pbxProject.SetBuildProperty(target, "SWIFT_VERSION", "4.2");
Xcodeで使うSwiftバージョンを指定します。これが無いと、「Swiftバージョンが未指定」という旨のエラーが出るはずです。
"4.2"の部分は、 使用しているXcodeがサポートしてるSwiftバージョンを使ってください 。
pbxProject.SetBuildProperty(target, "SWIFT_OBJC_INTERFACE_HEADER_NAME", "UnityFramework-Swift.h");
「Swiftの処理をiOSプラグインで呼び出し」の節(VibrationObjc.mm)で説明した、Swiftのヘッダーファイルの名前を指定しています。VibrationObjc.mm
で記述するヘッダー名と一致させれば、任意の名前を付けることができます。
pbxProject.AddFrameworkToProject(target, "CoreHaptics.framework", false);
CoreHapticsのフレームワークをXcodeに追加しています。
使用例
using UnityEngine;
public class VibrationTest : MonoBehaviour {
void Start() {
// Hapticsの初期化
VibrationUtil.SetupHapticEngine();
}
void Update() {
// 画面タッチした瞬間を検出
if (Input.GetTouch(0).phase == TouchPhase.Began) {
VibrationUtil.PlayHapticEngine(1f, 1f, 0.1f);
}
}
}
このコンポーネントをシーン上の適当なゲームオブジェクトにアタッチすれば、画面をタッチするたびにバイブレーションが発生します。簡単!
補足
その他、いろいろ実装に当たって必要だったことや気づいたことです。
■Haptics 対応端末かどうかの判定
APIにはそれらしいメソッドが見つからなかったので 「iPhone8以降、iOS13以上」 というCore Haptics の動作条件に従って、泥臭い方法で判定メソッド作ってみました。
using UnityEngine;
~~~中略~~~
public static bool IsSupportedHapticEngine() {
#if UNITY_ANDROID || UNITY_EDITOR
return false;
#endif
string versionString = SystemInfo.operatingSystem;
versionString = versionString.Replace("iOS ", "");
float version = -1f;
float.TryParse(versionString.Substring(0, 2), out version);
if (version < 13) {
// iOS13未満は未対応
return false;
}
string deviceString = SystemInfo.deviceModel;
if (!deviceString.Contains("iPhone")) {
// iPhone以外の端末(iPadなど)は未対応
return false;
}
deviceString = deviceString.Replace("iPhone", "");
float deviceID = -1f;
float.TryParse(deviceString.Substring(0, 2), out deviceID);
// デバイスモデル名が「iPhone10.0」以上(=iPhone8以降の機種)
// 参照:http://www.enterpriseios.com/wiki/iOS_Devices
IsSupportedHaptics = deviceID >= 10;
return IsSupportedHaptics;
}
この方法だと、新型のiPhoneやiPadが出るたびにHaptics対応してるか確認しないといけないので困った…。公式のAPIで判定できるのを待つしかないのかなぁ。
▼絶対にマネしてはいけない方法
#import <UIKit/UIKit.h>
bool isSupportedHaptics() {
NSNumber *supportLevel = [[UIDevice currentDevice] valueForKey:@"_feedbackSupportLevel"];
return [supportLevel intValue] == 2;
}
stack overflowのトピック で見かけた方法なのですが、プライベート変数でそれらしい数値を取ることができるらしく、「2
以外のときは Haptic 未対応端末」と判定できるみたいです。
が! 非公開APIを触るのはバイナリを解析すれば分かるので、余裕でAppStoreからリジェクトくらいます。 やめようね!
余談ですが、この指摘をくださった方曰く…
以前、実装の名前がたまたま非公開APIと完全一致しただけでもリジェクトされました T-T
恐ろしスギィ!
■アプリをバッググラウンドに入れたり端末をスリープさせると振動しなくなる
VibrationSwift.swift
で定義してる hapticEngine
が破棄されてる?
private void OnApplicationPause(bool pauseStatus) {
if (!pauseStatus) {
// アプリ復帰時、HapticEngine初期化
VibrationUtil.SetupHapticEngine();
}
}
ひとまず上記のように、アプリが復帰するたびに初期化処理を呼べば再現しなくなりましたが、リークしてそうな気がして怖い。どうなんでしょう。
■Haptics でUXデザインするときのコツ(公式ガイドライン)
Human Interface Guidelines - Haptics の、 Designing with Haptics の部分に「こういうことを意識してね」「こういうことに気を付けてね」という指標が書いてあったので、ざっくり要約してみました。
- 「何で今振動したの?」という体験がないようにすること
- 単品ではなく、目に映る演出や音とセットで振動させること
- むやみやたらに振動させてユーザーにストレスを与えないようにすること
- 振動が苦手な人も「このくらいならOFFにしなくてもいいか」くらいがGood
- 同じシチュエーションでは同じ振動をさせること(一貫性)
- なんでここでは振動しないの?はNG
- できるだけいろんな実機×人間で試すこと(ストレスに感じるかどうか個体差が凄い)
- オプションでOFFにできるようにすること
- 操作量やレスポンスの把握しやすさを向上させる使い方を意識すること
- 最大スワイプに達したとき、UIをタッチできたときなど、判断スピード向上できるのが望ましい
- カメラやマイク、ジャイロの妨げにならないように気を付けること
たぶんこういう旨のことが書いてました。いずれも納得の内容。
iPhoneのシステム標準で使われてる Haptics Feedback は凄くナチュラルで小気味良いので、ここに辿り着くまでにAppleでたくさんの試行錯誤があったんだろうなぁ…。その結果が知見となってこのガイドラインを生んだと思うので、まずは従ってみたほうが良いでしょう。
タッチディスプレイとの組み合わせでデザインするUX なので、コンシューマーゲームのバイブレーションとはまた違う考え方をしないといけないですね。
■OnPostprocessBuildで記述してるXcodeの設定箇所
CoreHaptics.framework
がなぜか一度Xcodeで直接追加しないとスクリプトによる追加が働かないっぽい挙動にハマったことがあったので、一応直接設定する場所も記載しておきます。(たぶんスクリプトで大丈夫ですが…念のため…)
▼CoreHaptics.framework
▼「SWIFT_OBJC_INTERFACE_HEADER_NAME」、「SWIFT_VERSION」
Swiftのバージョンは、ここで選択できる数字をスクリプトで指定すればよいかと
参考記事
▼Core Haptics
- Core Haptics 公式ドキュメント
- 忙しい人のためのCore Haptics
- iOS13で公開予定の「Core Haptics」を使って、Haptic Feedback (触覚フィードバック) するコードを書いてみた
-
Check if device supports UIFeedbackGenerator in iOS 10
- Haptics 対応端末かどうかの判定に関する stack overflow のトピック