회사에서 유니티로 채팅 기능을 만들 기회가 생겼다.
다른 레퍼런스 앱을 찾아보면, 게임처럼 유저들이 전체적으로 텍스트 채팅을 할 수 있게 하거나 (제페토), 유니티가 아닌 경우가 대부분이다. (마음, 커넥팅, 등등)
처음에는 사실 그렇게 어렵지 않게 해결할 수 있다고 생각했는데, 의외로 별것도 아닌 걸로 문제가 생겼다.
1. 백그라운드 처리
2.유니티 Textmeshpro의 InputField 문제
3. 키보드 높이
4. 이모지
내가 어떻게 이 문제를 해결했는지 공유하고자 한다. 꼭 유니티가 아니더라도 비슷한 기능을 구현하는 개발자 분들이 비슷한 문제가 생겼을 때 헤매지 않길 바라며..!
1. 백그라운드 처리
텍스트 채팅은 백그라운드에서 데이터를 받을 수 있어야 한다. 일단 어떻게 처리할지는 서비스 방향이나 필요성에 따라 적절하게 처리해 주면 되는데, 우리는 백그라운드에서 포어그라운드로 넘어올 때 읽지 않는 메시지를 모두 서버에서 받기로 했다. 백그라운드에서 별도 쓰레드를 열어서 웹소켓으로 지속적으로 데이터를 받았다가, 포어그라운드에서 처리해도 되지만, 당시에는 해당 기능이 개발 중에 있었다. 그리고, 나중에 읽지 않은 메시지 등을 처리할 때 지금처럼 구현하면 괜찮지 않을까 해서 서버 개발자 분께 제안했는데, 고맙게도 그렇게 만들어 주셨다.
유니티에서 iOS/안드로이드 모두 pause 됐을 때 콜백을 주는 이벤트 함수가 있다. 바로 OnApplicationPause !
void OnApplicationPause(bool isPause)
{
//isPause == false일 때 서버에 읽지 않은 메세지 요청
}
이런 식으로 요청만 잘해주면, 사실 그렇게 큰 문제없이 잘 해결할 수 있다.
2. 키보드 높이
채팅 기능의 기본적인 기능 중 하나가 키보드에서 뭘 입력하고 있는지 보여주는 UI이다. 카톡처럼 웬만한 채팅앱은 키보드 바로 위 인풋필드를 배치한다.
일단 앱에서는 키보드 높이에 맞춰 입력창을 띄워주는 방식을 채택했다. 여기서 상당한 시간을 썼다. 안드로이드와 iOS의 키보드 높이 또는 위치를 구하는 방식이 다르다. 유니티에서 제공하는 TouchScreenKeyboard 클래스의 area는 안드로이드에서 사용할 수 없다. 안드로이드와 iOS는 키보드 높이를 구하는 방법이 다르다. iOS는 키보드 높이에 대한 정보를 제공하기 때문에 그대로 가져와서 사용하면 되지만, AOS는 보이는 부분을 전체 스크린 높이를 빼서 구해야 한다.
안드로이드 키보드 높이 구하기
약간 부가설명을 하자면, 현재 유니티의 Activity에서 키보드를 제외한 부분을 구해서, 전체 해상도 높이에서 빼줘서 키보드 높이를 구할 수 있다.
private static float GetKeyboardHeight_AOS()
{
using (AndroidJavaClass unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
{
using (AndroidJavaObject rect = new AndroidJavaObject("android.graphics.Rect"))
{
view.Call("getWindowVisibleDisplayFrame", rect);
return Screen.height - rect.Call<int>("height"); //- decorHeight;
}
}
}
iOS 키보드 높이 구하기
iOS는 키보드에 대한 정보를 몇 가지 notification 이벤트에 가지고 있다. UIKeyboardFrameEndUserInfoKey에서 키보드에 대한 위치, 크기, 애니메이션 등 정보를 아래 플러그인처럼 가지고 올 수 있다.
void RegisterKeyboardWillShowNotifications()
{
[[NSNotificationCenter defaultCenter] addObserverForName:UIKeyboardWillShowNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
// Get the keyboard frame from the notification
CGRect keyboardFrame = [note.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
NSLog(@"Keyboard Manager Plugin: keyboardFrame - %@", NSStringFromCGRect(keyboardFrame));
UIScreen *mainScreen = [UIScreen mainScreen];
CGFloat unitScale = mainScreen.scale;
// Convert the keyboard frame to screen coordinates
// fixedCoordinateSpace => consistent coordinate when orientation is changed.
keyboardFrame = [UIScreen.mainScreen.coordinateSpace convertRect:keyboardFrame toCoordinateSpace:UIScreen.mainScreen.fixedCoordinateSpace];
NSLog(@"Keyboard Manager Plugin: bottom Padding - %f", bottomPadding * unitScale);
NSLog(@"Keyboard Manager Plugin: size.height - %f", keyboardFrame.size.height * unitScale);
// Calculate the keyboard height
float keyboardHeight = (keyboardFrame.size.height) * unitScale;
NSLog(@"Keyboard Manager Plugin: Scaled keyboardHeight - %f", keyboardHeight);
// Send the keyboard height to Unity
UnitySendMessage("IOSKeyboardControlReceiver", "OnKeyboardWillShow", [@(keyboardHeight) stringValue].UTF8String);
}];
}
void RegisterKeyboardWillChangeFrameNotification()
{
[[NSNotificationCenter defaultCenter] addObserverForName:UIKeyboardWillChangeFrameNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
// Get the keyboard frame from the notification
CGRect keyboardFrame = [note.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
//NSLog(@"Keyboard Manager Plugin: animationCurve - %@", note.userInfo[UIKeyboardAnimationCurveUserInfoKey]);
//NSLog(@"Keyboard Manager Plugin: animationDuration - %@", note.userInfo[UIKeyboardAnimationDurationUserInfoKey]);
//NSLog(@"Keyboard Manager Plugin: keyboardFrame - %@", NSStringFromCGRect(keyboardFrame));
UIScreen *mainScreen = [UIScreen mainScreen];
CGFloat unitScale = mainScreen.scale;
// Convert the keyboard frame to screen coordinates
// fixedCoordinateSpace => consistent coordinate when orientation is changed.
keyboardFrame = [UIScreen.mainScreen.coordinateSpace convertRect:keyboardFrame toCoordinateSpace:UIScreen.mainScreen.fixedCoordinateSpace];
// Get Bottom Safe Area
UIWindow *window = UIApplication.sharedApplication.windows.firstObject;
CGFloat bottomPadding = window.safeAreaInsets.bottom;
// Calculate the keyboard height
float frameOrigin = keyboardFrame.origin.y;
float heightInUnit = frameOrigin >= mainScreen.bounds.size.height ?
0 : (mainScreen.bounds.size.height - bottomPadding - keyboardFrame.origin.y);
float keyboardHeight = heightInUnit * unitScale;
// Send the keyboard height to Unity
UnitySendMessage("IOSKeyboardControlReceiver", "OnKeyboardFrameChanged", [@(keyboardHeight) stringValue].UTF8String);
}];
}
Safe area와 Canvas Scaling
키보드 높이 구하는 건 사실 애플이나 안드로이드 개발자 문서만 봐도 쉽게 만들 수 있다. 근데 문제는 Safe area와 canvas scaling이다.
실제로 위에서 구현된 함수를 구해서 입력창 UI 높이를 조절하면 이렇게 된다.
보통 유니티로 UI를 개발할 때 반응형 UI를 위해 Canvas scaling을 많이 쓸 것이다. 나중에 글로 정리할 기회가 생기면 자세히 설명하겠습니다. 아래 사진에 보이는 것처럼 이미지가 화면 밖으로 빠져나가지 않도록 컴포넌트 크기를 해상도에 맞춰서 조절한다.
스케일링을 할 때는 Reference resolution를 기준으로 canvas의 UI를 어떻게 조절할 건지가 결정된다.
그래서 canvas scaling을 사용할 경우 네이티브 플러그인에서 구한 높이값이 보이는 것과 다르다. 그래서 종횡비에 따라서 길쭉한 기기는 실제 키보드 높이보다 더 높을 것이고, 폴드처럼 넓은 기기에서는 실제 키보드 높이보다 더 낮을 것이다.
개발할 때 이 사실을 모르고, 왜 키보드 높이를 구했는데 안 맞지? 라면서 온갖 뻘짓을 많이 했다. 네이티브 코드가 잘못됐다고 생각해서 개발자 페이지도 수없이 읽었고, 구글링도 정말 많이 했다. 근데 대부분 정보는 유니티가 아닌 다른 개발환경이라 그런지 정답을 찾을 수 없었다.
정말 우연히 같은 코드로 갤럭시 A52s 노트20이 다른 걸 보고 깨달았다. 아. 그거다...
나는 일단 Constant Pixel Canvas를 사용해서 해결했다. 처음에는 스케일링한 양만큼 역으로 나눠주면 되겠다고 생각했는데, 생각보다 잘 안 됐다. 기기별로, 해상도별로 다 대응하자니 공수가 너무 많이 들어 constant pixel canvas로 해결했다.
참고로 iOS 네이티브에서 나오는 숫자는 픽셀이 아니라 별도의 unit이 있다. 스크린의 스케일을 곱해줘서 픽셀을 구해야 한다.
3. TMPro InputField
카톡을 써보면 키보드가 위에 올라와있을 때 + 버튼도 누를 수 있고, 카톡 내용도 슬라이드 할 수 있다. 유니티에서 그렇게 하려면 일단 키보드가 켜져 있을 때 다른 오브젝트와 상호작용할 수 있어야 한다.
유니티에서 제공하는 TextMeshPro의 InputField는 기본적으로 UGUI 시스템 자체에서 다른 오브젝트가 선택되면 이전에 선택된 오브젝트는 해제하도록 돼있다.즉, 오브젝트가 릴리즈 되면 키보드를 숨기도록 돼있다.
// TMP_Inputfield클래스 LateUpdate 함수의 일부
if (isFocused && InPlaceEditingChanged())
DeactivateInputField();
// DeactivateInputField함수의 일부
if (m_SoftKeyboard != null)
{
m_SoftKeyboard.active = false;
m_SoftKeyboard = null;
}
DeactivateInputField함수는 퍼블릭 함수이기 때문에 기존 코드를 상속받아서 수정할 수도 없다.
그래서 그냥 모든 코드를 복사해서 새로운 스크립트를 만들었다. 해당 부분만 이렇게 바꿔서 다른 행동을 할 때 키보드만 숨김처리하면 모든 게 해결된다. 대신, 키보드를 숨기고 싶을 때 수동으로 아래와 같이 함수를 만들어 호출해줘야 한다.
public void ReleaseKeyboard()
{
if(m_SoftKeyboard == null) return;
m_SoftKeyboard.active = false;
m_SoftKeyboard = null;
}
이 문제는 구글링해도 답은 안 나오고, 아래 유니티 포럼에서 보면 아주 오래된 문제이기도 하다. 공식답변으로 해결해 줄 것처럼 하더니 결국 안 됐나 보다.
유니티에서 안드로이드는 키보드가 표시될 때 뒤에 있는 어떤 UI도 선택할 수 없다. 실제로 해보면, 화면을 두 번 눌러야 UI가 실제로 선택될 것이다. 유니티 포럼에 공식 답변에 따르면, 2022.1부터는 안드로이드와 iOS가 같은 동작을 한다고 한다. 그전 버전에서는 아래 속성을 꺼주면(false) 키보드가 떠있는 상황에서도 UGUI 오브젝트를 선택할 수 있다.
TouchScreenKeyboard.Android.consumesOutsideTouches
인터넷에 너무 정보가 안 나와서 처음으로 포럼에 답변 올려봤다.. ㅎㅎ
4. 이모지
자체적으로 입력창을 만들면서 또 다른 문제가 있었다...
유니티는 별도의 activity에서 별도의 렌더링을 통해 실행이 되기 때문에 네이티브 이모지를 지원하지 않는다. 그래서 이모지 스프라이트 에셋을 만들어서 유니코드 별로 인덱싱을 하는 방법도 있고, 이번에 TMP 3.2.0 pre.4에서부터 color emoji 폰트 기능이 추가됐다. 사실 앞 기능이랑 비슷한 원리지만, 좀 더 고화질 이모지를 사용할 수 있고, 유니코드가 자동으로 인덱싱 되는 듯하다.
뭐 어찌 됐든 이런 식으로 이모지를 사용하는데, hide mobile input 기능을 켜게 되면 키보드 위에 이상하게 거슬리는 입력창을 끌 수 있는데, 이때 안드로이드에서 이모지를 연속으로 사용할 수 없다.
참 어이없는 상황이지만, 해결은 해야지.. 다시 아까 새로 만든 InputField 스크립트를 쭉 봤다. 네이티브 키보드를 호출할 때 Unity는 TouchScreenKeyboard.Open을 사용한다. 그럼 이 함수에 키보드 유형이라던가, character validation 등 여러 가지 옵션이 들어간다. 그중에 HideMobileInput이 있는데, 이 부분을 강제로 켜주면 된다.
// InputField ActivateInputFieldInternal 함수의 일부
if (inputSystem != null && inputSystem.touchSupported)
{
TouchScreenKeyboard.hideInput = true;
}
하.. 정말 유니티를 하다 보면 정말 이해 안 되는 부분 중 하나가 이런 거다. 네이티브 친화적이지 않아서 이런 거 하나하나를 개발하거나, 이런 식으로 야매(?)로 수정해줘야 한다. 이런 부분을 좀 신경 써주면 더 많은 사람들이 유니티로 앱 개발도 해볼 텐데.. 조금 아쉽다.
5. 후기
진짜 키보드 높이 문제는 며칠 동안 밤에 자꾸 생각나서 미칠뻔했다. 지나고 보면 뭐 당연한 문제일 수도 있는데, 정말 에너지 소모도 많고, 힘든 피처 중 하나였다. 그래도 이번에 공부도 하고 개발도 하면서 네이티브 코드에 대한 이해도가 많이 높아졌다. 네이티브, 특히 iOS에서 플러그인 만드는 방법도 좀 더 잘 이해하게 됐고, 개발 문서에 대한 이해도도 높아졌다. 다음에 네이티브 코드를 만 질 일이 또 있을지는 모르겠지만, 더 잘할 수 있겠다는 자신감이 생겼다랄까...?
정말 이렇게까지 해야 되나 싶으신 분은 어떤 사람이 만든 무료 에셋이 있는데, Advanced InputField 2라고 있는데, 도전해 보는 것도 나쁘지 않을 것 같다. 그래도 테스트는 안 해봐서 사용하라고는 못하겠다...
이 모든 문제를 해결하려면 방법은 세 가지이다.
1. 내가 모든 네이티브 코드(키보드 열기/닫기. 키보드 위 버튼 기능, 키보드 유형 설정, 등)를 짠다.
2. 야매로 한다.
3. 에셋을 쓴다.
근데 나는 에셋을 쓰면 또 연구 기간이 필요한 건 매한가지라 유니티의 기능을 좀 더 자세히 공부해 보기로 결정한 것이다.
유니티로 텍스트 채팅 개발해 보기 끗!
6. 참고자료
keyboardFrameEndUserInfoKey | Apple Developer Documentation
A user info key to retrieve the keyboard’s frame at the end of its animation.
developer.apple.com
Feedback - Feature Request: Control of mobile TouchScreenKeyboard visibility on focus change
I posted on the forums about this previously and it was suggested to me that I submit it as a bug fix/feature request. I submitted a bug report and was...
forum.unity.com
Keyboard hides when tap anywhere
I'm use TMP Input field on Android. There are inputfield and some buttons in my UI. When I begin input text, keyboard shows, but when I tap on button,...
forum.unity.com
Unity - Scripting API: TouchScreenKeyboard.Android.consumesOutsideTouches
This is relevant when the on-screen keyboard is visible. When true, Unity ignores touch input outside of the on-screen keyboard area. When false, Unity processes touch input outside of the on-screen keyboard area as it would do if the keyboard wasn't on-sc
docs.unity3d.com
Feedback - Feature Request: Control of mobile TouchScreenKeyboard visibility on focus change
I posted on the forums about this previously and it was suggested to me that I submit it as a bug fix/feature request. I submitted a bug report and was...
forum.unity.com
'프로그래밍 > 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] 진동 구현하기 (Android/iOS) (0) | 2023.12.10 |