진동 관련 일을 하면서 가장 번거롭고 힘들게 테스트하는 과정이다. 유니티에서 제공하는 Handheld.Vibrate
를 사용해도 되지만, 이건 단순한 진동만 제공할 뿐, 원하는 형태 (길이, 세기, 등)로 커스터마이즈 할 수 없다.
진동은 직접 trial and error로 구현을 해야 하는데, 구현하고, 빌드해서 테스트하고, 다시 고치고, 빌드해서 테스트하는 과정을 정말 많이 반복했다. 게다가, iOS는 기기별로 햅틱모듈이 그나마 비슷비슷해서 괜찮았는데, 안드로이드는 진동은 제조사 별로, 기기별로 다 다르기 때문에 어떤 기기를 기준으로 테스트해야 할지도 고민이었다.
원하는 진동을 구현하려면, 플랫폼 별로 네이티브 구현을 해야한다. 네이티브 API는 자료도 많고, 공식 문서대로 하면 어렵지 않게 구현할 수 있었지만, AOS의 경우 iOS처럼 사전에 정의된 진동 타입이 그렇게 많지 않다. 그래서 다른 분들은 이 유틸리티를 통해 테스트 시간을 줄일 수 있게 하고자 이 글을 써본다.
Git
GitHub - pktony/VibrationForAndroidiOS-Unity
Contribute to pktony/VibrationForAndroidiOS-Unity development by creating an account on GitHub.
github.com
테스트 앱
https://play.google.com/store/apps/details?id=com.IsDororok.VibrationTester&hl=en_GB
Vibration Tester – Apps on Google Play
Vibration tester made with Unity 3D
play.google.com
이번에 진동 공부를 하면서, 구글플레이에 테스트 앱도 같이 출시했습니다!
사용법
Git repository의 ./Assets/Scripts/ 폴더 안 다음 파일을 모두 복사한다.
- VibrationInstance.cs
- VibrationAndroid.cs
- VibrationEditor.cs
- VibrationIOS.cs
- VibrationUtil.cs
요구사항
- Android
- API Level >= 26
- Permssion :
- iOS
- OS Version : >= 10.0
세부 구현
Android
안드로이드는 API레벨에 따라 구현 방법이 다르다. API 31부터 VibrationManager라는 클래스가 생겨서 사용하면 되지만, 기존 Vibrator 클래스도 아직 deprecate된 상태가 아니고, 기능이 있는 것 같은데 새로 만든 이유를 아직 모르겠다. 이번에는 android.os.Vibrator클래스를 이용해서 구현해 보겠다.
근데 조심해야 할 건, 기존 vibrate 몇 가지 함수는 API 26에서 deprecate 됐기 때문에 사용할 수 없었다.
Vibrator | Android Developers
developer.android.com
문서에 나와있는 대로, android.os.VibrationEffect를 파라미터로 받는 오버로드된 함수를 사용할 예정이다.
일단 시작 전, 안드로이드 진동 권한이 필요하다. 유니티 프로젝트 안에 Android Manifest 파일에 다음 권한을 추가해 주자.
<uses-permission android:name = "android.permission.VIBRATE"/>
일단 유니티 기본 activity를 통해서 vibrator 클래스를 불러온다.
private AndroidJavaObject vibrator = null;
public AndroidJavaObject Vibrator
{
get
{
if (vibrator == null)
{
var player = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
var currentActivity = player.GetStatic<AndroidJavaObject>("currentActivity");
vibrator = currentActivity.Call<AndroidJavaObject>("getSystemService", "vibrator");
Debug.Log($"Vibration Util : Android Vibrator {vibrator}");
}
return vibrator;
}
}
파라미터로 넘겨줄 VibrationEffect클래스도 불러온다.
private AndroidJavaClass vibrationEffectClass;
public AndroidJavaClass VibrationEffectClass
{
get
{
vibrationEffectClass ??= new AndroidJavaClass("android.os.VibrationEffect");
return vibrationEffectClass;
}
}
사실 이건 안 해줘도 될 것 같긴 한데, 혹시 모르니 진동 모듈이 있는지도 확인해 준다. 그리고, VibrationEffect를 사용하는 API가 26 레벨에 추가된 거라, 26 이상인지도 확인해 준다.
public override bool IsVibrationAvailable()
{
var hasVibrator = Vibrator.Call<bool>("hasVibrator");
return hasVibrator && AndroidApiLevel >= 26;
}
그리고, 이제 VibrationEffect를 구현해야 하는데, 안드로이드에서 지원하는 진동에는 여러 가지 종류가 있다. 대표적으로 :
- oneShot : 일정시간 동안 한 번 진동
- predefined : 안드로이드에서 사전 정의한 진동의 종류. (바로 뒤에서 자세히 설명)
- waveform : 진동을 특정 시간, 타이밍을 이용해서 구현. 반복도 설정 가능
- 안드로이드의 predefined 진동은 5가지가 있다. iOS에 비하면 너무 부실하고, 실행해 보니 별 특별한 게 없어서 사용하지 않았다.
그리고, 이건 iOS에서 몇 가지 사전 정의된 햅틱을 설명해 놓은 문서인데, iOS에서 사전 정의된 진동을 안드로이드에서도 비슷하게 사용하면 좋을 것 같아서 이번 유틸리티를 만들면서 많이 참고했다. 물론, 햅틱 모듈이 달라서, 느낌은 많이 다르지만, 최대한 비슷하게 만들어 봤다.
[ 글 하단에 링크 있습니다 ]
그다음부터는 사실, 노가다(?)의 영역이다. 이런 식으로, trial and error로 여러 번 테스트하면서 만들었다. VibrationType은 iOS predefined 타입을 이용했다. 왜냐하면, 앱에서 iOS는 경험하는 진동이 안드로이드랑 다르면 안드로이드 사용자는 얼마나 억울한가.. (?). 자세한 설명은 iOS 세부구현에서 설명하겠다.
public override void Vibrate(VibrationType vibrationType, float intensity)
{
if (!IsVibrationAvailable())
{
Debug.LogWarning("Vibration Util - Your device does not support Vibration");
return;
}
this.intensity = intensity;
switch (vibrationType)
{
case VibrationType.Peek:
CreateOneShot(5, 150);
break;
case VibrationType.Pop:
CreateOneShot(10, 150);
break;
case VibrationType.Nope:
var pattern = new long[4]
{
0, 10,
80, 5,
};
var patternAmplitude = new int[4]
{
0, 200,
0, 200,
};
CreateWaveform(pattern, patternAmplitude, -1);
break;
case VibrationType.Heavy:
CreateOneShot(10, 150);
break;
case VibrationType.Medium:
CreateOneShot(10, 100);
break;
case VibrationType.Light:
CreateOneShot(10, 50);
break;
case VibrationType.Rigid:
CreateOneShot(5, 100);
break;
case VibrationType.Soft:
CreateOneShot(15, 50);
break;
case VibrationType.Error:
pattern = new long[8]
{
0, 10,
100, 10,
100, 10,
100, 15
};
patternAmplitude = new int[8]
{
0, 100,
0, 100,
0, 150,
0, 100
};
CreateWaveform(pattern, patternAmplitude, -1);
break;
case VibrationType.Success:
pattern = new long[4]
{
0, 10,
200, 10
};
patternAmplitude = new int[4]
{
0, 150,
0, 200
};
CreateWaveform(pattern, patternAmplitude, -1);
break;
case VibrationType.Warning:
pattern = new long[4]
{
0, 10,
250, 10
};
patternAmplitude = new int[4]
{
0, 150,
0, 100
};
CreateWaveform(pattern, patternAmplitude, -1);
break;
default:
CreateOneShot(500, -1);
break;
}
}
iOS
Object-C로 UI Kit 라이브러리를 사용해 구현했다. 개인적으로 안드로이드보다, iOS 네이티브 코드 구현이 더 어려운 것 같다. iOS도 이제는 swift를 사용하는 시점에서 아마 legacy로 Object-C를 남겨놓지 않았나 싶다. 근데, 잠시 조사해 본 바로는 유니티에서 swift로 네이티브 구현하는 게 만만찮은 일인 것 같다... 다음에 도전...!
UIKit에는 총 4가지 햅틱 관련 클래스가 있다.
- UIFeedbackGenerator
- UIImpactFeedbackGenerator
- UINotificationFeedbackGenerator
- UISelectionFeedbackGenerator
FeedbackGenerator가 base 클래스이고, 나머지가 이 클래스를 상속한 클래스이다. 특이하게, FeedbackGenerator에는 prepare라는 함수가 있는데, 햅틱 모듈을 미리 준비시켜놓는다고 한다. 아마 햅틱 모듈이 원운동을 하면서 진동을 주는 형식일 텐데, 멈춰있다가 갑자기 시작하게 되면 레이턴시도 있고, 진동 사이사이 간섭이 생길 수 있어서 이런 걸 만들지 않았나 싶다.
배터리 소모 향상을 위해서 다음 세 가지 상황에서 햅틱 모듈이 idle 상태로 돌아간다고 한다.
나머지는 사실, 개발자 페이지에 나와있는 대로만 구현하면 잘 된다. Object-C로 간단하게 이런 식으로 구현하면 된다.
extern "C"
{
long getFeedbackStyle(const char* style)
{
if (strcmp(style, "Heavy") == 0)
return UIImpactFeedbackStyleHeavy;
else if (strcmp(style, "Medium") == 0)
return UIImpactFeedbackStyleMedium;
else if (strcmp(style, "Light") == 0)
return UIImpactFeedbackStyleLight;
else if (strcmp(style, "Rigid") == 0){
if (@available(iOS 13.0, *))
return UIImpactFeedbackStyleRigid;
else return -1;
}
else if (strcmp(style, "Soft") == 0){
if (@available(iOS 13.0, *))
return UIImpactFeedbackStyleSoft;
else return -1;
}
else return -1;
}
void Vibrate(int _n)
{
AudioServicesPlaySystemSound(_n);
}
void _impactOccurred(const char* style)
{
long feedbackStyle = getFeedbackStyle(style);
if(feedbackStyle == -1)
return;
UIImpactFeedbackGenerator* generator = [[UIImpactFeedbackGenerator alloc] initWithStyle: (UIImpactFeedbackStyle)feedbackStyle];
[generator prepare] ;
[generator impactOccurred] ;
}
void _impactOccurredWithIntensity(const char* style, float intensity)
{
long feedbackStyle = getFeedbackStyle(style);
if(feedbackStyle == -1)
return;
UIImpactFeedbackGenerator* generator = [[UIImpactFeedbackGenerator alloc] initWithStyle: (UIImpactFeedbackStyle)feedbackStyle];
[generator prepare] ;
[generator impactOccurredWithIntensity : intensity] ;
}
void _notificationOccurred(const char* style)
{
UINotificationFeedbackType feedbackStyle;
if (strcmp(style, "Error") == 0)
feedbackStyle = UINotificationFeedbackTypeError;
else if (strcmp(style, "Success") == 0)
feedbackStyle = UINotificationFeedbackTypeSuccess;
else if (strcmp(style, "Warning") == 0)
feedbackStyle = UINotificationFeedbackTypeWarning;
else return;
UINotificationFeedbackGenerator* generator = [[UINotificationFeedbackGenerator alloc] init];
[generator prepare] ;
[generator notificationOccurred : feedbackStyle] ;
}
전체적인 구조는 사용하는 언어만 다를 뿐 안드로이드랑 크게 다르지 않다.
Vibration Type
iOS가 햅틱 관련해서 predefined 햅틱 타입이 잘 돼 있는 것 같다.
Notification
Face ID를 사용한다면, 아마 최근 아이폰에서 가장 많이 경험하는 햅틱일 것이다. Face ID 인증에 성공하면 성공 피드백이 여기 Success에 해당된다. 인증에 실패하면 Error 햅틱을 준다.
Impact
기본적인 진동 유형이다. 사실 내가 좀 둔감해서 그런가, 실제로 느껴보면 차이를 잘 모르겠다. 눈감고 테스트하면 절대 모를 듯...
Selection
이 부분도 iOS 사용자라면 많이 경험해 봤을 것이다. 스크롤을 하면 항목이 중앙에 걸릴 때마다 짧은 진동이 오는데, 이걸로 구현하면 될 것 같다. 근데 이번 유틸리티에서 이 부분은 구현하지 않았는데, 필요하면 추가할 예정이다.
Code Overview
Git에 public으로 공개를 하면서, readme에 code overview를 작성하면 좋을 것 같아서 써봤다. 다른 외부 라이브러리를 사용하면 항상 이런 문서를 보게 되는데, 이런 문서를 잘 쓴 라이브러리를 보면 정말 대단하다는 생각이 든다. 연습해 두면 좋을 것 같다!
원본링크
[GitHub - pktony/VibrationForAndroidiOS-Unity
Contribute to pktony/VibrationForAndroidiOS-Unity development by creating an account on GitHub.
github.com](https://github.com/pktony/VibrationForAndroidiOS-Unity)
VibrationUtil
Vibrate
public abstract void Vibrate(VibrationType vibrationType);
Details
Vibrates using pre-defined types. See Vibrationtype
VibrationType
enum VibrationType
Details
Default = 1352, Peek = 1519, Pop = 1520, Nope = 1521, Heavy, Medium, Light, Rigid, Soft, Error, Success, Warning
AmplitudeType
public class AmplitudeType {}
Details
Vibration Amplitude Types
These values are adjusted by trial and errors on Samsung Galaxy.
LengthType
public class LengthType {}
(Customized) Vibration length (duration) types
These values are adjusted by trial and error on Samsung Galaxy.
VibrationAndroid
public class VibrationAndroid : VibrationInstance { }
Details
Implements vibration method for AOS
IsVibrationAvailable
protected override bool IsVibrationAvailable();
Details
Checks if the Android device supports the vibration API. Checks 2 functions : HasVibrator && API Level >= 26
Returns
True : Available
False : Not Available
VibrationIOS
public class VibrationIOS : VibrationInstance { }
Details
Implements vibration method for iOS
IsVibrationAvailable
protected override bool IsVibrationAvailable();
Details
Does not check availability for iOS for now. Always return true.
Returns
True : Available
후기
사실 진동 기능 자체는 자료도 많고, 그렇게 어렵지 않게 구현할 수 있다. 그래도 이게 또 하려면 여간 귀찮은 작업이 아니다. 직접 구현하셔도 되지만, 제가 만든 유틸리티를 기반으로 조금씩 조절하면서 구현하면 시간단축에 많이 도움이 되지 않을까 싶다!
아까 위에서 말한 것처럼, Apple 개발자 페이지 Playing haptics 페이지를 많이 참고했다. 상황에 따라서 적절한 진동을 넣어주면 사용자 경험도 많이 좋아질 거라고 생각한다.
Git 링크에 모든 코드 공개 중입니다. 궁금하신 분이나, 사용하실 분들은 글 위에 링크 있습니다! 진동 세부 조절 해보실 분들은 안드로이드 앱 사용하시면서 조절해 보세요!
참고자료
[VibrationEffect | Android Developers
android.inputmethodservice
developer.android.com](https://developer.android.com/reference/android/os/VibrationEffect)
[Vibrator | Android Developers
android.inputmethodservice
developer.android.com](https://developer.android.com/reference/android/os/Vibrator#hasVibrator\(\))
[UINotificationFeedbackGenerator | Apple Developer Documentation
A concrete feedback generator subclass that creates haptics to communicate successes, failures, and warnings.
developer.apple.com](https://developer.apple.com/documentation/uikit/uinotificationfeedbackgenerator)
[Playing haptics | Apple Developer Documentation
Playing haptics can engage people’s sense of touch and bring their familiarity with the physical world into your app or game.
developer.apple.com](https://developer.apple.com/design/human-interface-guidelines/playing-haptics)
'프로그래밍 > Unity 3D' 카테고리의 다른 글
[Unity] Advanced Inputfield 사용기 (6) | 2024.11.27 |
---|---|
[Unity] Swift로 네이티브 플러그인 만들기 (feat. AVAudioSession) (3) | 2024.04.13 |
[Unity] 에디터 자동 컴파일 (Refresh) 끄기 (0) | 2024.01.24 |
[Unity] Git으로 Custom Package 만들기 (0) | 2024.01.21 |
[Unity] 텍스트 채팅을 구현하며 생긴 일 (2) | 2024.01.18 |