Liberent-Dev’s blog

株式会社リベル・エンタテインメントのテックブログです。

ハプティクス(Haptics)の調査とその所感

こんにちは!
システム開発部のK.Mです。

今月から再開となります。どうぞよろしくお願いします。
今回から複数回にわたり、新しいメンバーが執筆した記事をお届けします!


こんにちは!
同じく、システム開発部のさわやか太陽です。

今回は実は、皆さんが何気なく体感しているハプティクスについて調査しましたので、 その結果と所感をお伝えしようと思います。
よろしくお願いします。

社内からの調査依頼

  • ゲーム内UIにハプティクスを入れてほしい。
    • 最近はハプティクスを使っているアプリが増えている。
      • ボタン押下などのUI操作に対して、導入可能かを調査してほしい。

ハプティクス(Haptics)とは?

「実際にモノに触れているような感触」をフィードバックする技術。
バイブレーションは単純に振動させる機能であり、ハプティクスは「振動や動きを与える」もの。
(振動フィードバックとも呼ばれている)

体感イメージは以下です。

  • 左が軽くボタンを押下したパターン
  • 右がボタンを押し込んだパターン

引用:ハプティクス UXデザイン

振動の強弱が付けられて体感が表現されています。
さらに詳細を知りたい場合は以下URL先を参考にしてください。

参考:ハプティクス UXデザイン

結論

  • 実装可能ではあるが、ゲーム仕様に合わせた相談が必要。
    • ハプティクス機能を呼び出しても無反応な端末もある。
      • iOSは問題なし。
      • Androidは手元にあった端末で確認。
        • Pixel 6aはハプティクスのパターン全対応している。
        • Galaxy M23 5Gは一部対応している。半分近くは無反応で機種依存がある。
        • Androidに関しては機種によって対応非対応にばらつきがある。
  • ボタン押下などの「短い表現であれば」ハプティクスもバイブレーションもどちらを使っているか体感として気がつかない。
    • 単純に仕様によってはバイブレーションでも良いかもしれない。

ハプティクス実装(Unity)

Android

using UnityEngine; 

public static class AndroidHaptic 
{
    private static AndroidJavaObject unityActivity;
    private static AndroidJavaObject decorView;

    static AndroidHaptic() 
    {
        //decorViewを取得 
        using(AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) 
        {
            unityActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity"); 
            decorView = unityActivity.Call<AndroidJavaObject>("getWindow").Call<AndroidJavaObject>("getDecorView"); 
        }
    }

    public static void HapticFeedback(int feedbackConstant)
    {
        bool result = decorView.Call<bool>("performHapticFeedback", feedbackConstant);

        // ※注意点あり(後述)返り値が正しいとは限らない
        Debug.Log($"Haptic feedback result: {result}");
    }
}

参考:振動Typeの一覧

iOS

iOSHaptic.cs

using System.Runtime.InteropServices;

public static class iOSHaptic 
{
    [DllImport("__Internal")]
    private static extern void PerformHaptic(int type); 

    public static void HapticFeedback(int type) 
    {
        PerformHaptic(type);
    }
}

HapticFeedback.mm

#import <UIKit/UIKit.h>

extern "C"{
    void PerformHaptic(int type) { 
        if (@available(iOS 10.0, *)) {
            switch (type) { 
                case 0:{ 
                    UISelectionFeedbackGenerator *generator = [[UISelectionFeedbackGenerator alloc] init]; 
                    [generator selectionChanged]; 
                    break; 
                }
                case 1:{ 
                    UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight]; 
                    [generator impactOccurred]; 
                    break; 
                }
                case 2:{ 
                    UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium]; 
                    [generator impactOccurred]; 
                    break; 
                }
                case 3:{ 
                    UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleHeavy]; 
                    [generator impactOccurred]; 
                    break; 
                }
                case 4:{ 
                    UINotificationFeedbackGenerator *generator = [[UINotificationFeedbackGenerator alloc] init]; 
                    [generator notificationOccurred:UINotificationFeedbackTypeSuccess]; 
                    break; 
                }
                case 5:{ 
                    UINotificationFeedbackGenerator *generator = [[UINotificationFeedbackGenerator alloc] init]; 
                    [generator notificationOccurred:UINotificationFeedbackTypeWarning]; 
                    break; 
                }
                case 6:{
                    UINotificationFeedbackGenerator *generator = [[UINotificationFeedbackGenerator alloc] init]; 
                    [generator notificationOccurred:UINotificationFeedbackTypeError]; 
                    break; 
                }
                default: 
                    break; 
            }
        }
    }
}

iOSは呼び出し関数が少々違います。
参考:UIImpactFeedbackStyle
参考:UINotificationFeedbackType

【問題発生】Android端末でハプティクスが一部反応しない

Pixel 6aは反応するのにGalaxy M23 5Gは反応しない問題が発生。
機種依存によりハプティクス対応していないパターンが存在した。

ハプティクス対応/非対応を検知できるか?

結論:Androidで検知が出来ないケースがある。
Pixel 6aは検知できるが、Galaxy M23 5Gは検知できない、挙動詳細は以下です。

調査内容:Callした返り値を利用し検知できるかを調査。

  • 返り値が正しくない問題が発生した。
    • Pixel 6aは振動したらtrue、しなかったらfalse。
    • Galaxy M23 5Gは振動したらtrue、振動しなくてもtrue
      • 見当違いなTypeを渡してもtrueで返ってくる。
      • これが原因でハプティクス or バイブレーションの切り分けができなかった。
int feedbackConstant = ハプティクスTypeを指定

bool hapticSuccess = decorView.Call<bool>("performHapticFeedback", feedbackConstant);
if (hapticSuccess){}

iOSは?

ハプティクス対応しているか? = OSバージョンを満たしているか
であるためアプリインストールのバージョン条件を合わせれば問題なし。

  • UIImpactFeedbackStyleやUINotificationFeedbackTypeなどを使用するUIFeedbackGenerator関連はiOS10以降 且つ iPhone7以降。
  • CHHapticEngineなどを使用するCore Haptics関連はiOS13以降 且つ iPhone8以降。

バイブレーション実装

Android

using UnityEngine; 

public static class AndroidHaptic
{
    private static AndroidJavaObject unityActivity;
    private static AndroidJavaObject vibrator;
    private static bool hasVibrator;
    private static bool hasAmplitudeControl;
    
    static AndroidHaptic()
    {
        using (AndroidJavaObject context = unityActivity.Call<AndroidJavaObject>("getApplicationContext"))
        using (AndroidJavaClass contextClass = new AndroidJavaClass("android.content.Context"))
        {
            vibrator = context.Call<AndroidJavaObject>("getSystemService", contextClass.GetStatic<string>("VIBRATOR_SERVICE")); 
            hasVibrator = vibrator != null && vibrator.Call<bool>("hasVibrator");
            hasAmplitudeControl = vibrator != null && vibrator.Call<bool>("hasAmplitudeControl");
        }
    } 
    
    public static void Perform(HapticType type) 
    {
        // Hapticsと似たような長さは20~50ほど
        long duration = 20;
        
        if(hasAmplitudeControl)
        {
            // 強度付きバイブレーション指定(API 26 以降)
            int amplitude = 255;
            vibrator.Call("vibrate", AndroidVibrationEffect(duration, amplitude));
        }
        else
        {
            vibrator.Call("vibrate", duration);
        }
    }
    
    private static AndroidJavaObject AndroidVibrationEffect(long milliseconds, int amplitude)
    {
        using(AndroidJavaClass vibrationEffectClass = new AndroidJavaClass("android.os.VibrationEffect"))
        {
            return vibrationEffectClass.CallStatic<AndroidJavaObject>("createOneShot", milliseconds, amplitude);
        }
    }
}

iOS

iOSHaptic.cs

using System.Runtime.InteropServices; 
using UnityEngine; 

public static class iOSHaptic
{ 
    
    private const int Vibrate_Default        = 4095;      //標準バイブ(約400ms)
    private const int Vibrate_ImpactLight    = 1520;      //軽い振動(ImpactLight 相当)
    private const int Vibrate_ImpactMedium   = 1521;      //中くらいの振動(ImpactMedium 相当)
    private const int Vibrate_ImpactHeavy    = 1011;      //強い振動(ImpactHeavy 相当)
    private const int Vibrate_Notification   = 1522;      //二回連続振動(Notification風)
    
    [DllImport("__Internal")]
    private static extern void PerformCustomVibration(int soundId);
    
    public static void Perform(int type)
    {
        PerformCustomVibration(type);
    }
}

HapticFeedback.mm

#import <UIKit/UIKit.h>
#import <AudioToolbox/AudioToolbox.h>

extern "C" {
    void PerformCustomVibration(int id) {
        AudioServicesPlaySystemSound(id);
    }
}

余談:AudioServicesPlaySystemSoundはバイブレーションも呼べるがシステムサウンドも呼べる機能らしい。

バイブレーションでハプティクスと同じことはできるか?

結論:代替は可能(ただし短い振動であれば)。
前述通り、振動の強弱でハプティクスが構成されているため、長い表現であればあるほどバイブレーションだけでは厳しい。

余談

個人的な体感ではあるが、ハプティクスの方が歯切れがあり気持ち良い。
バイブレーションと比較したら気がつくレベル。何も言われなかったら気がつかないかもしれない。

バイブレーションで更にハプティクスっぽいことができるか?

バイブレーションにも強弱や長さの指定ができるので、任意で連続呼び出しをすれば恐らく可能。
(機種依存があるかなど、詳細は未調査)

ただし、精度は落ちるであろうかつ、バイブレーション機能だけで対応するとユーザーにこちらの想定していない印象を与えてしまうかもしれない。

ハプティクスとバイブレーション、同時呼び出しはどちらが優先されるか?

テストケースをまとめました。

Pixel 6a

テストケース ハプティクス有効端末
動作結果(Pixel 6a)
希望動作
ハプティクス実行後同時フレームで
振動を実行する
ハプティクス&振動の
両方が動作する
振動のみ動作する
振動実行後同時フレームで
ハプティクスを実行する
ハプティクスが動作する ハプティクスが動作する
ハプティクス実行の1フレーム後
振動を実行する
ハプティクス&振動の
両方が動作する
振動が動作する
振動実行の1フレーム後
ハプティクスを実行する
ハプティクスが動作する ハプティクスが動作する

Galaxy M23 5G

テストケース ハプティクス不完全端末
動作結果(Galaxy M23 5G)
希望動作
ハプティクス実行後同時フレームで
振動を実行する
振動が動作する 振動が動作する
振動実行後同時フレームで
ハプティクスを実行する
ハプティクスが動く場合はハプティクスが動作、
ハプティクスが動かない場合は振動が動作する
振動が動作する
ハプティクス実行の1フレーム後
振動を実行する
振動が動作する 振動が動作する
振動実行の1フレーム後
ハプティクスを実行する
ハプティクスが動く場合はハプティクスが動作、
ハプティクスが動かない場合は振動が動作する
振動が動作する

簡易的にまとめると、どちらも後発で呼び出した内容が優先されます。


リベル・エンタテインメントでは、このような最新技術などの取り組みに興味のある方を募集しています。
もしご興味を持たれましたら下記サイトにアクセスしてみてください。
https://liberent.co.jp/recruit/