From 39ef252c2e7f66924cf01f9ebbd95868bf12ad75 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sat, 4 Dec 2021 22:14:02 +0900 Subject: [PATCH] ebiten: Add VibrationOptions to specify intensity (#1891) Updates #1452 --- examples/vibrate/main.go | 12 +++-- internal/driver/ui.go | 2 +- internal/uidriver/glfw/ui.go | 2 +- internal/uidriver/js/ui_js.go | 4 +- internal/uidriver/mobile/vibrate_android.go | 58 ++++++++++++++++----- internal/uidriver/mobile/vibrate_ios.go | 8 +-- vibrate.go | 26 +++++++-- 7 files changed, 84 insertions(+), 28 deletions(-) diff --git a/examples/vibrate/main.go b/examples/vibrate/main.go index 79b81c88c..609a512ce 100644 --- a/examples/vibrate/main.go +++ b/examples/vibrate/main.go @@ -32,15 +32,21 @@ const ( ) type Game struct { - touchIDs []ebiten.TouchID - gamepadIDs []ebiten.GamepadID + touchIDs []ebiten.TouchID + gamepadIDs []ebiten.GamepadID + touchCounter int } func (g *Game) Update() error { g.touchIDs = g.touchIDs[:0] g.touchIDs = inpututil.AppendJustPressedTouchIDs(g.touchIDs) if len(g.touchIDs) > 0 { - ebiten.Vibrate(200 * time.Millisecond) + g.touchCounter++ + op := &ebiten.VibrateOptions{ + Duration: 200 * time.Millisecond, + Intensity: 0.5*float64(g.touchCounter%2) + 0.5, + } + ebiten.Vibrate(op) } g.gamepadIDs = g.gamepadIDs[:0] diff --git a/internal/driver/ui.go b/internal/driver/ui.go index 9fcab0cd7..7d87f1fc6 100644 --- a/internal/driver/ui.go +++ b/internal/driver/ui.go @@ -63,7 +63,7 @@ type UI interface { SetScreenTransparent(transparent bool) SetInitFocused(focused bool) - Vibrate(duration time.Duration) + Vibrate(duration time.Duration, intensity float64) Input() Input Window() Window diff --git a/internal/uidriver/glfw/ui.go b/internal/uidriver/glfw/ui.go index 4135b30ec..22cf2de4c 100644 --- a/internal/uidriver/glfw/ui.go +++ b/internal/uidriver/glfw/ui.go @@ -1673,6 +1673,6 @@ func (u *UserInterface) setOrigPos(x, y int) { u.origPosY = y } -func (u *UserInterface) Vibrate(duration time.Duration) { +func (u *UserInterface) Vibrate(duration time.Duration, intensity float64) { // Do nothing. } diff --git a/internal/uidriver/js/ui_js.go b/internal/uidriver/js/ui_js.go index 409ddf895..d7a13093a 100644 --- a/internal/uidriver/js/ui_js.go +++ b/internal/uidriver/js/ui_js.go @@ -648,7 +648,9 @@ func (u *UserInterface) SetInitFocused(focused bool) { u.initFocused = focused } -func (u *UserInterface) Vibrate(duration time.Duration) { +func (u *UserInterface) Vibrate(duration time.Duration, intensity float64) { + // intensity is ignored. + if js.Global().Get("navigator").Get("vibrate").Truthy() { js.Global().Get("navigator").Call("vibrate", float64(duration/time.Millisecond)) } diff --git a/internal/uidriver/mobile/vibrate_android.go b/internal/uidriver/mobile/vibrate_android.go index 87c066fa6..bdd0b8928 100644 --- a/internal/uidriver/mobile/vibrate_android.go +++ b/internal/uidriver/mobile/vibrate_android.go @@ -21,30 +21,41 @@ import ( ) /* - #include #include #include +#include + // Basically same as: // // Vibrator v = (Vibrator)getSystemService(Context.VIBRATOR_SERVICE); -// v.vibrate(millisecond) -// -// TODO: As of API Level 26, the new API should be Used instead: -// -// Vibrator v = (Vibrator)getSystemService(Context.VIBRATOR_SERVICE); -// v.vibrate(VibrationEffect.createOneShot(millisecond, VibrationEffect.DEFAULT_AMPLITUDE)) +// if (Build.VERSION.SDK_INT >= 26) { +// v.vibrate(VibrationEffect.createOneShot(milliseconds, intensity * 255)) +// } else { +// v.vibrate(millisecond) +// } // // Note that this requires a manifest setting: // // // -static void vibrateOneShot(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, int64_t milliseconds) { +static void vibrateOneShot(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, int64_t milliseconds, double intensity) { JavaVM* vm = (JavaVM*)java_vm; JNIEnv* env = (JNIEnv*)jni_env; jobject context = (jobject)ctx; + static int apiLevel = 0; + if (!apiLevel) { + const jclass android_os_Build_VERSION = (*env)->FindClass(env, "android/os/Build$VERSION"); + + apiLevel = (*env)->GetStaticIntField( + env, android_os_Build_VERSION, + (*env)->GetStaticFieldID(env, android_os_Build_VERSION, "SDK_INT", "I")); + + (*env)->DeleteLocalRef(env, android_os_Build_VERSION); + } + const jclass android_content_Context = (*env)->FindClass(env, "android/content/Context"); const jclass android_os_Vibrator = (*env)->FindClass(env, "android/os/Vibrator"); @@ -59,10 +70,29 @@ static void vibrateOneShot(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, (*env)->GetMethodID(env, android_content_Context, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;"), android_context_Context_VIBRATOR_SERVICE); - (*env)->CallVoidMethod( - env, vibrator, - (*env)->GetMethodID(env, android_os_Vibrator, "vibrate", "(J)V"), - milliseconds); + if (apiLevel >= 26) { + const jclass android_os_VibrationEffect = (*env)->FindClass(env, "android/os/VibrationEffect"); + + const jobject vibrationEffect = + (*env)->CallStaticObjectMethod( + env, android_os_VibrationEffect, + (*env)->GetStaticMethodID(env, android_os_VibrationEffect, "createOneShot", "(JI)Landroid/os/VibrationEffect;"), + milliseconds, (int)(intensity * 255)); + + (*env)->CallVoidMethod( + env, vibrator, + (*env)->GetMethodID(env, android_os_Vibrator, "vibrate", "(Landroid/os/VibrationEffect;)V"), + vibrationEffect); + + (*env)->DeleteLocalRef(env, android_os_VibrationEffect); + + (*env)->DeleteLocalRef(env, vibrationEffect); + } else { + (*env)->CallVoidMethod( + env, vibrator, + (*env)->GetMethodID(env, android_os_Vibrator, "vibrate", "(J)V"), + milliseconds); + } (*env)->DeleteLocalRef(env, android_content_Context); (*env)->DeleteLocalRef(env, android_os_Vibrator); @@ -74,10 +104,10 @@ static void vibrateOneShot(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, */ import "C" -func (u *UserInterface) Vibrate(duration time.Duration) { +func (u *UserInterface) Vibrate(duration time.Duration, intensity float64) { _ = app.RunOnJVM(func(vm, env, ctx uintptr) error { // TODO: This might be crash when this is called from init(). How can we detect this? - C.vibrateOneShot(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx), C.int64_t(duration/time.Millisecond)) + C.vibrateOneShot(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx), C.int64_t(duration/time.Millisecond), C.double(intensity)) return nil }) } diff --git a/internal/uidriver/mobile/vibrate_ios.go b/internal/uidriver/mobile/vibrate_ios.go index 01db64cac..300c9443e 100644 --- a/internal/uidriver/mobile/vibrate_ios.go +++ b/internal/uidriver/mobile/vibrate_ios.go @@ -42,7 +42,7 @@ package mobile // return nil; // } // -// static void vibrate(double duration) { +// static void vibrate(double duration, double intensity) { // if (@available(iOS 13.0, *)) { // static BOOL initializeHapticEngineCalled = NO; // static CHHapticEngine* engine = nil; @@ -64,7 +64,7 @@ package mobile // (id)(CHHapticPatternKeyEventParameters):@[ // @{ // (id)(CHHapticPatternKeyParameterID): CHHapticEventParameterIDHapticIntensity, -// (id)(CHHapticPatternKeyParameterValue): @1.0, +// (id)(CHHapticPatternKeyParameterValue): [NSNumber numberWithDouble:intensity], // }, // ], // }, @@ -103,9 +103,9 @@ import ( var vibrationM sync.Mutex -func (u *UserInterface) Vibrate(duration time.Duration) { +func (u *UserInterface) Vibrate(duration time.Duration, intensity float64) { vibrationM.Lock() defer vibrationM.Unlock() - C.vibrate(C.double(float64(duration) / float64(time.Second))) + C.vibrate(C.double(float64(duration)/float64(time.Second)), C.double(intensity)) } diff --git a/vibrate.go b/vibrate.go index 5e51fcd77..70954c003 100644 --- a/vibrate.go +++ b/vibrate.go @@ -18,20 +18,38 @@ import ( "time" ) -// Vibrate vibrates the device. +// VibrateOptions represents the options for device vibration. +type VibrateOptions struct { + // Duration is the time duration of the effect. + Duration time.Duration + + // Intensity is the strength of the device vibration. + // The value is in between 0 and 1. + Intensity float64 +} + +// Vibrate vibrates the device with the specified options. // // Vibrate works on mobiles and browsers. // +// On browsers, Intensity in the options is ignored. +// // On Android, this line is required in the manifest setting to use the vibration: // // // +// On Android, Intensity in the options is recognized only when the API Level is 26 or newer. +// Otherwise, Intensity is ignored. +// +// On iOS, Vibrate works only when iOS version is 13.0 or newer. +// Otherwise, Vibrate does nothing. +// // Vibrate is concurrent-safe. -func Vibrate(duration time.Duration) { - uiDriver().Vibrate(duration) +func Vibrate(options *VibrateOptions) { + uiDriver().Vibrate(options.Duration, options.Intensity) } -// VibrateGamepadOptions represents the options to vibrate a gamepad. +// VibrateGamepadOptions represents the options for gamepad vibration. type VibrateGamepadOptions struct { // Duration is the time duration of the effect. Duration time.Duration