React NativeでHaptic(触覚フィードバック)を使うにはexpo-hapticsやreact-native-haptic-feedback 等のライブラリを使う方法があります。
ただこれらはUIFeedbackGeneratorを使っており、標準で用意されているフィードバック(notification
, impact
, selection
)しか利用することができません。ゲームで使われるような独自のパターンの振動(Custom Haptics)を発生させるにはどうしたらよいでしょうか?
Custom Hapticsを使うにはiOS13以降で導入されたCore HapticsというAPIを利用する必要があります。これをReact Nativeで使うためのライブラリとしてreact-native-core-haptics-api があります。これを使ってCustom Hapticsを試してみたいと思います。
iOS 13以上でないと使えないため、Podfileで以下を指定する必要があります。
platform :ios, '13.0'
APIはSwiftのAPIとできる限り近い形式になっています。以下は2つの振動を間隔を置いて発生させる例です。
import { HapticEngine, HapticPatternType } from "react-native-core-haptics-api"
const pattern: HapticPatternType = {
hapticEvents: [
{
duration: 0.3,
// "HapticContinuous" と "HapticTransient"が指定可能
eventType: { rawValue: "HapticContinuous" },
relativeTime: 0,
parameters: [
{ parameterID: { rawValue: "HapticIntensity" }, value: 0.7 },
{ parameterID: { rawValue: "HapticShapness" }, value: 0.4 },
],
},
{
duration: 0.3,
eventType: { rawValue: "HapticContinuous" },
relativeTime: 0.5,
parameters: [
{ parameterID: { rawValue: "HapticIntensity" }, value: 0.7 },
{ parameterID: { rawValue: "HapticShapness" }, value: 0.4 },
],
},
],
}
await HapticEngine.start(undefined)
await HapticEngine.makePlayer(pattern, undefined)
const startTime = 0
await HapticEngine.startPlayerAtTime(pattern, startTime, undefined)
setTimeout(async () => {
await HapticEngine.stop(undefined)
}, 3000)
一つの振動をHapticEventとして定義し、それを組み合わせてHapticPatternを作成します。relativeTime
は開始から何秒後に発生させるかを指定します。
引数でundefined
を渡している箇所がいくつかありますが、これを省略するとエラーになってしまうので注意してください。
使い終わったらHapticEngine.stop()
で終了させます。
eventType
にはHapticContinuous
とHapticTransient
が指定可能です。振動を継続的に発生させるか、一瞬発生させるかの違いです。ただこれは実際に体感してみないと分かりづらいため、以下のパターンで違いを理解してみます。まずHapticTransient
のイベントを継続的に発生させた後、HapticContinues
に切り替えます。
const pattern: HapticPatternType = {
hapticEvents: [...Array(20).keys()].map(i => ({
duration: 0.1,
eventType: {
rawValue: i < 10 ? "HapticTransient" : "HapticContinuous",
},
relativeTime: 0.2 * i,
parameters: [
{
parameterID: { rawValue: "HapticIntensity" },
value: 0.5,
},
],
})),
}
実際に実行してみると、前半は一つひとつのイベントがポコッ、ポコッという感じで瞬間的に発生し、後半はブッ、ブッという感じで長めになっているのがわかります。
eventTypeにはオーディオ用のAudioContinuous
とAudioCustom
も利用できますが今回は試しません。
イベントのパラメータとしてHapticShapness
を設定できます。これも違いを理解するために以下のパターンを試してみます。前半はSharpnessの値を低くし、後半は高くしています。
const pattern: HapticPatternType = {
hapticEvents: [...Array(20).keys()].map(i => ({
duration: 0.1,
eventType: { rawValue: "HapticContinuous" },
relativeTime: 0.2 * i,
parameters: [
{
parameterID: { rawValue: "HapticSharpness" },
value: i < 10 ? 0.01 : 1,
},
],
})),
}
実際に実行すると、前半と後半で振動の仕方が異なっているのがわかります。 前半は少し鈍く(重く)、後半は鋭く(軽く)感じます。
イベントのパラメータにはHapticIntensity
という値も設定できます。前半は値を低く、後半は値を高くしてみます。
const pattern: HapticPatternType = {
hapticEvents: [...Array(20).keys()].map(i => ({
duration: 0.1,
eventType: { rawValue: "HapticContinuous" },
relativeTime: 0.2 * i,
parameters: [
{
parameterID: { rawValue: "HapticIntensity" },
value: i < 10 ? 0.01 : 1,
},
],
})),
}
実際に実行すると振動の強さが前半と後半で異なるのが確認できます。
独自でゲームで使うようなパターンを作成してみます。一定間隔でイベントを発生させますが、前半と後半でイベントのパラメータを変更しています。前半は長さ・間隔が長めで、後半では短くなります。Intensityも後半強くしています。
const pattern = {
hapticEvents: [...Array(30).keys()].map(i => ({
duration: i < 10 ? 0.2 : 0.15,
relativeTime: [...Array(i).keys()].reduce(
(sum, current) => sum + (current < 10 ? 0.25 : 0.18),
0,
),
eventType: { rawValue: "HapticContinuous" },
// relativeTime: i * 0.3,
parameters: [
{
parameterID: { rawValue: "HapticSharpness" },
value: 0.1,
},
{
parameterID: { rawValue: "HapticIntensity" },
value: i < 15 ? 0.8 : 1,
},
],
})),
}
実際に実行してみると、前半はブー、ブー、ブー、という感じで貯めるような感じに振動し、後半になると強くなりブ、ブ、ブ、ブ、ブと一気に放つような振動になります。文章表現だと伝わりにくい部分があるので、興味あるかたは実際に試してみることをおすすめします。
実際に試したサンプルコードはこちらになります。