유니티와 함께하는 안드로이드

Jun.30.2020 박준선

APP

소개

저는 신사업부문에서 Thiiing(띠잉)서비스를 만들고 있는 안드로이드 개발자 박준선입니다.
띠잉 안드로이드 앱에 유니티를 subview 로서 적용한 경험을 설명하겠습니다.


안드로이드 : 유니티님~ 함께 하실…?

띠잉이라는 안드로이드 프로젝트에서는 디바이스의 카메라를 통해 얻은 프레임 데이터에서 얼굴과, 얼굴 요소를 트래킹하여 좌표를 얻고, 해당 좌표에 각 시나리오별로 선별된 각종 3D 효과를 입힌 프레임을 사용자에게 영상화 해서 보여주는 기능이 구현되어야 했습니다.

해당 기능을 구현하기 위해서 여러 방법이 있지만, 기회비용을 고려했을 때 멀티플랫폼 프로그래밍까지 가능한 유니티와 함께 하기로 했습니다.

유니티 : 넹, 근데 가능?

유니티는 게임 엔진인데, 띠잉 서비스는 게임이 아닙니다.

AR 기술이 활용된 영상 촬영 경험을 제공하고, 이를 공유힐 수 있는 수단을 제공함으로써 사용자들이 교류 할 수 있는 소셜 네트워크 서비스입니다.

유니티를 통해 구현된 부분은 띠잉 서비스의 일부 기능으로서 안드로이드 프레임워크로 구현된 화면과 공존할 수 있도록 작업해주어야 했습니다.

유니티는 멀티플랫폼 프로그래밍이 가능하고 이를 위한 각종 환경설정 도구들을 제공합니다.
properties
그러나 본질은 게임 엔진입니다.
게임이 아닌 일반적인 어플리케이션을 만들어 왔던, 안드로이드 프레임워크 위에서만 열심히 개발해오던 입장에서 안드로이드를 타겟으로 빌드된 유니티를 맞닥뜨리게 되면 어떻게 해결해야 할지 막연한 문제들이 보입니다.

1) Full screen 화면을 기본으로 구현됩니다. 상태바와 네비게이션바가 *UnityPlayer에서, 또 유니티 내부 JNI 호출을 통해서 보이지 않도록 관리됩니다.
2) UnityPlayer 에서 제공하는 생명주기 요소 중 종료에 해당하는 메소드 호출 시 현재 Process 가 종료되도록 설계되어있습니다.

  • UnityPlayer: 유니티의 구동 + 안드로이드<->유니티 연동을 위한 클래스. 유니티 에디터에서 안드로이드를 타겟으로 export 하면 generate 됨

모바일게임에 대한 경험을 생각해보면 저런 특성이 있다는 것이 이해가 됩니다. 하지만 일반적인 어플리케이션에서 위에 서술된 요소들이 구현될 이유가 제한적이고,
또 띠잉 서비스 또한 마찬가지로 결정된 UI/UX 측면에서 위에 서술된 요소들과는 반대로 구현되어야 했기 때문에 깊은 고민에 빠졌습니다.

안드로이드 : 가..가능!

우리는 답을 찾을 것이다. 늘 그랬듯이 …

개발자가 된 이후 접하게 된 여러 문제에 대하여 일면식도 없는 어느 먼 나라의 개발자를 통해서, 내 옆자리 동료 개발자를 통해서 혹은 본인 스스로 해결책을 찾아 왔다는 것이 매번 새삼스럽게 신기하고 긍정적인 경험이었습니다.
위에 서술한 문제에 대해서도 난감했지만 결국 여러 해결책을 검토할 수 있었습니다.

Full screen이 강제되는 이슈에 대해서서는 UnityPlayer 의 생성 전/후로 Window 의 각종 플래그를 조작하는 방법을 생각하고 구현했으나 Window 가 재조정 되는 과정에서 생기는 화면 덜컥 거리는 등의 이슈가 있어 다른 방법을 찾아봐야 했습니다.
UnityPlayer 클래스를 살펴보니 주입된 Context 를 Activity 로 캐스팅해서 작업하는 여러 부분들이 있는데, 그 중 Window 에 접근해서 각종 플래그를 설정하는 부분을 찾을 수 있었습니다. 결론적으로 UnityPlayer 생성자 파라미터 Context 에 Activity 를 주입하지 않는 것으로 원하는 윈도우 형태를 가져갈 수 있었습니다.

프로세스를 종료하는 이슈에서는 아래 방법들을 생각해 볼 수 있었습니다.

  • 유니티가 구동되는 프로세스를 분리
  • 프로세스가 종료되도 정상적으로 운용될 수 있도록 다른 컴퍼넌트(서비스) 등의 프로세스를 분리
  • ‘종료’에 해당하는 메소드 호출 시점 검토

유니티가 됐던, 안드로이드가 됐던 프로세스를 분리하는 것도 선택지었으나 다음과 같은 문제로 현재 버전까지는 프로세스 분리 작업은 보류하고 있습니다.

  • 화면을 분리해야 하기 때문에 생기는 UX 변경에 대한 사용자 경험 측면 고려
  • 멀티 프로세스로 어플리케이션을 구현할 때 필요로하는 부가적인 작업과 향후 유지보수

UnityPlayer 에서는 안드로이드 Activity 의 이벤트 메소드를 트리거로 삼는 메소드와 생명주기에 맞춰진 메소드들이 만들어져 있습니다.

unityPlayer.resume()
unityPlayer.pause()
unityPlayer.lowMemory()
unityPlayer.destroy()
unityPlayer.configurationChanged(newConfig)

그 중 destory 메소드를 호출하지 않거나, 호출시점을 변경하는 방식을 고려해서 작업이 되어있습니다.
그런데, UnityPlayer 에 Activity Context 를 주입해서 Activity 의 생애와 맞물리도록 설계하면 destroy 메소드 호출이 필수가 됩니다.(종료콜이 오지 않은 상태로 UnityPlayer 를 Activity 생성과 맞물려 생성하게 되면 크래쉬 :()

위에 서술한 Full screen 의 이슈를 해결하기 위해서도 생성자 파라미터로 Application Context 를 주입해서 UnithPlayer 의 생명주기를 Activty 가 아닌 Application 에 맞게 구현해주어야 했습니다.

유니티 : 계속 이런 방식으로 공존 할 수 있을까요?

UnityPlayer 는 FrameLayout를 상속받고 있습니다. UnityPlayer 를 Application Context 을 통해 생성하겠다는 말은 View 에 Application Context 를 주입했다는 말이 됩니다. 아시다시피 View 의 Context 주입을 일반적인(Activity 를 주입하는) 방법과 다르게 해서 파생되는 이슈에 대해서는 고민이 필수적이었지요.
또한 UnityPlayer 에서 메인스레드에서 동작하도록 구현된 몇몇의 기능들도 동작하지 않게됩니다.

if(context instanceof Activity) {
    ((Activity) context).runOnUiThread(...);
}

주입된 Contxet 가 Application 이기 때문에 동작하지 않는 몇 가지의 기능들을 다행스럽게도 메소드 오버라이딩으로 해결했습니다만 아직 몇가지 문제가 있습니다.
위에 서술한 View 의 context 참조 문제도 포함해서 근본적으로 유니티에서 Activity 의존적이게 디자인해 놓은 설계를 뒤짚을 이유에 대한 의구심이 들어, 안드로이드 파트에서는 별도 프로세스로 분리하기 위한 작업을 진행하고 있습니다.

??? : 아니, 근데 유니티로 AR 영상 촬영 화면 어떻게 구현했어요?

유니티에서만 가능한 구현, 안드로이드에서 가능하거나 주체가 되어야할 부분들을 나눠서 유니티 개발자분들과 협업을 진행했습니다.

  • 안드로이드: *코스튬 데이터와 디바이스 카메라로 부터 받은 프레임 데이터를 보내주기
  • 유니티: 카메라 프레임 데이터를 통해 얼굴요소 트래킹 후 전달받은 코스튬 데이터를 이용해 각 코스튬 시나리오에 맞춰 얼굴요소에 리소스 마스킹

*코스튬 : 얼굴에 스티커를 붙이거나, 마스크를 씌우거나, 게임요소를 넣은 것. 이 단어를 사용해서 정보에 대한 의사소통을 했습니다.

안드로이드 사이드에서 서버로부터 코스튬 목록을 제공받아 로컬 DB 정보를 업데이트하고, 각 코스튬 별 리소스(유니티 용어로 에셋번들) 버전 업데이트, 리소스 파일 다운로드의 과정을 거쳐 유니티로 전달하도록 했습니다.

// 짧은 설명을 위해 네이밍은 실제 프로덕트와는 다름!   
getCostumes()
    .also 
    .map 
    .also 

// Unity Game Object 에 특정 함수를 파라미터를 포함해서 콜 할 수 있다.
UnityPlayer.UnitySendMessage("GameObjectName", "Load", costume)

유니티 사이드에서는 전달받은 코스튬 정보와 리소스 경로를 제공받아 코스튬이 동작할 수 있도록 리소스를 로드하고, 로드된 코스튬에서 활용할 수 있게 안드로이드에서 전달받은 카메라 프레임을 통해서 얼굴 요소 좌표를 생성합니다.

void Load(string costume) {
    Costume costume = deserialize(costume);
}

유니티 <-> 안드로이드 간 카메라 데이터 연동

// 안드로이드 사이드에서 카메라에 접근하여 데이터를 얻을 수 있는 클래스를 선언, 유니티에서는 패키지명 포함한 클래스 이름 + 생성자 파라미터로 다음과 같이 접근 가능함. 
private AndroidCamera androidCamera = new AndroidJavaObject("com.example.DeviceCameraManager", parameter);

// 매 프레임마다 발생하는 유니티 Event 메소드
void Update() {
    // 다음과 같이 특정 클래스(데이터 스트럭쳐)를 반환 받을 수 있다.
    var frame = androidCamera.Call<AndroidJavaObject>("getFrame");
}
class DeviceCameraManager(width, height....) {

    private var lastFrame: Frame? = null

    override fun onPreviewFrame(
        byteArray: ByteArray?,
        camera: Camera?
    ) {
        lastFrame = Frame(byteArray, ...)        
    }

    fun getFrame() : Frame? {
        val frame = lastFrame
        lastFrame = null
        return frame
    }
}

큰 흐름은 위와 같지만, 과정중에 서로 상태를 전달하거나 특정 명령을 해야 할 때도 있습니다.
예를들면 코스튬의 선택, 촬영 버튼, 카메라 방향 선택 등등의 UI 요소는 유니티화면 위에 안드로이드에서 직접 구현했고 해당 버튼들의 클릭 이벤트에 따라 유니티 쪽에 전달하면 유니티 쪽에서는 해당 작업을 진행후 완료 콜백을 주는 일종의 핑퐁 전략을 취했습니다.
ex) 코스튬 선택

UnitySendMessage("Start", ...) // 유니티로 코스튬 시작 명령 전달
void Start() {
    ... // 코스튬 시작관련 로딩
    AndroidJavaClass costumeManager = new AndroidJavaClass(..);
    // 다음과 같이 Static 메소드를 호출 할 수 있다.
    costumeManager.CallStatic("onStarted", ...); // 안드로이드로 시작관련로딩 끝 전달 
}
// 유니티와 협의된 이름의 클래스를 만들고 스태틱 메소드를 구성해 놓습니다.
@JvmStatic
fun onStarted(...) {
    // 선택된 상태 갱신
}

서로 간에 데이터, 명령 전달등의 작업은 쉽게 가능하지만 타이밍 이슈, 콜 순서보장 등의 이슈가 있습니다. 그 외에도 여러가지 문제가 있지만, 안드로이드<->유니티 개발자간 협업을 통해 해결해나가며 촬영 화면을 구현했습니다.
개발자 모두가 처음 구현해보는 프로젝트 타입이기에 서로 여러 삽질 공유를 통해 지속적으로 개선해 나가고 있습니다.

Unity 의 U 자도 모르는 안드로이드 개발자의 적용 후기

어느 정도 개발이 익숙해져서 인지, 안드로이드 사이드에서 새로운 기술이나 라이브러리가 나왔다하면 적용해보고 싶은 욕망이 매번 생깁니다.
하지만 기초적이고 근본적인 부분에 대해 들여다 볼 기회는 갈수록 적어졌지요.

유니티를 적용하면서 유니티에 대한 기초적인 지식과 더불어 안드로이드 기본이 되는 Activity, Application context 에 대해 다시 한 번 상기할 수 있었고, 프로세스의 분리의 필요성으로 프로세스에 대한 고찰도 할 수 있었습니다.
서술하진 않았지만, 메모리 크래쉬에 대해서 분석하면서 훌륭한 동료덕분에 Addr2line 을 이용한 symbolicate 방법 또한 알게 되었구요 🙂

물론 한정적인 시간내에서 문제들을 해결하다보니 혹여나 팩트가 아니거나 잘못된 방법을 사용한 것을 글로써 남겨 문제가 되진 않을까 염려스럽습니다.
새로운 것을 배우고 적용함에 있어서 그런 이슈가 있었고 이렇게 해결해보았구나 정도로 읽어주셨으면 싶고 혹시나 다른 좋은 방법을 적용하셨던 비슷한 경험이 있으신분들은 댓글로 노하우 전파 부탁드리겠습니다!
행복한 나날들 보내십시오 화이팅입니다~!


광고

띠잉앱을 설치해서 사용해보세요 🙂
[IOS](https
[Android](https

띠잉 공식 SNS도 운영하고 있어요.
[Instagram](https
[Facebook](https