Skip to content

Instantly share code, notes, and snippets.

@HofiOne
Last active March 22, 2026 15:10
Show Gist options
  • Select an option

  • Save HofiOne/85bf05784ae112b31ed95dc47c87944e to your computer and use it in GitHub Desktop.

Select an option

Save HofiOne/85bf05784ae112b31ed95dc47c87944e to your computer and use it in GitHub Desktop.
Native Android device vibration
if UNITY_ANDROID
// Cache Android vibrator objects to avoid allocations on every call
private static AndroidJavaObject cachedVibrator = null;
private static bool supportsAmplitudeControl = false;
private static bool vibratorInitialized = false;
private static void InitializeAndroidVibrator()
{
if (vibratorInitialized)
return;
vibratorInitialized = true;
try {
using (AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
using (AndroidJavaObject currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity")) {
// Cache the vibrator - don't dispose it, we'll reuse it
cachedVibrator = currentActivity.Call<AndroidJavaObject>("getSystemService", "vibrator");
if (cachedVibrator != null && cachedVibrator.Call<bool>("hasVibrator")) {
// Check API level once and cache the result
// Android API 26+ (Oreo 8.0) supports VibrationEffect with amplitude control
using (AndroidJavaClass buildVersion = new AndroidJavaClass("android.os.Build$VERSION")) {
int sdkInt = buildVersion.GetStatic<int>("SDK_INT");
supportsAmplitudeControl = sdkInt >= 26;
}
}
else {
cachedVibrator = null;
supportsAmplitudeControl = false;
}
}
}
catch (System.Exception e) {
if (GameLog.Enabled()) {
GameLog.LogError($"Android Vibrator initialization failed: {e.Message}");
}
cachedVibrator = null;
supportsAmplitudeControl = false;
}
}
public static void Vibrate(float intensity, float duration)
{
#if ! UNITY_EDITOR
return;
#endif
// Initialize vibrator on first use
if (false == vibratorInitialized)
InitializeAndroidVibrator();
if (cachedVibrator == null)
return;
try {
// Match iOS behavior: clamp intensity and enforce minimum duration
intensity = Mathf.Clamp(intensity, 0.0f, 1.0f);
duration = Mathf.Max(0.02f, duration); // iOS minimum: 20ms
// Convert duration from seconds to milliseconds
long durationMs = (long)(duration * 1000f);
if (supportsAmplitudeControl) {
// iOS uses sharpness=0.2 (rumble-like). Android motors are harsher,
// so reduce intensity and use non-linear curve to better match iOS feel
// Intensity curve: square root for softer ramp-up (more rumble-like)
float adjustedIntensity = Mathf.Sqrt(intensity) * 0.75f; // Softer mapping
// Map 0.0-1.0 to 40-220 (avoid extremes, match iOS rumble characteristic)
int amplitude = Mathf.Clamp((int)(adjustedIntensity * 180f + 40f), 40, 220);
using (AndroidJavaClass vibrationEffectClass = new AndroidJavaClass("android.os.VibrationEffect")) {
AndroidJavaObject vibrationEffect = vibrationEffectClass.CallStatic<AndroidJavaObject>(
"createOneShot", durationMs, amplitude);
cachedVibrator.Call("vibrate", vibrationEffect);
vibrationEffect.Dispose();
}
}
else {
// Fallback for older Android versions (no intensity control)
cachedVibrator.Call("vibrate", durationMs);
}
}
catch (System.Exception e) {
if (GameLog.Enabled()) {
GameLog.LogError($"Android Vibrate failed: {e.Message}");
}
}
}
#endif // #if UNITY_ANDROID
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment