이번 글에서는 Unity as a Library(UAAL) 방식을 활용하여, 유니티로 개발한 앱을 기존 안드로이드 프로젝트나 iOS(Xcode) 프로젝트에 통합하는 방법을 알아보겠습니다. 이를 통해 유니티에서 구현한 기능을 네이티브 모바일 앱 환경에서 직접 동작하도록 적용할 수 있습니다.
간단하게 3D 모델링으로 레벨 배치만 되어있는 프로젝트를 샘플로 사용하겠습니다.
유니티에서 Android로 빌드하기 전에 Minimum API Level이 몇인지 확인하고 네이티브 안드로이드 앱과 최대한 맞추는 것을 추천합니다.
주의, 유니티의 Minimum API Level 버전은 높은데, 네이티브 프로젝트의 Mimimum API Level 버전이 낮은 경우, 네이티브 프로젝트에서 세팅 다 하고 빌드해보면 빌드 에러 납니다.
Build Profiles에서 Android로 Export할 준비를 합니다, Export Project를 활성화 하고, Export를 눌러서 Android Project로 추출합니다.
Export가 완료된 프로젝트를 Android Studio에서 열면 다음과 같이 2개의 부분으로 구성되어 있습니다.
대부분 블로그나 포럼에서는 unityLibrary를 가져다 쓰거나 unityClass.jar과 기타 파일을 수동적으로 욺겨서 네이티브 앱에 연동하는 자료들이 많습니다. 이 방법도 틀린 방법은 아니지만 좀 더 사용성이 편한 방법을 알아봅시다.
Android Module을 aar 형태로 빌드하여 제 3자 고객사나 프로젝트에 aar 파일 하나만 전달하면 되도록 해볼 것입니다.
unityLibrary/manifests폴더로 이동하여 AndroidManifest.xml
을 선택하고 다음 내용을 제거합니다.
제거하지 않을 경우, 네이티브 안드로이드 프로젝트에서 최종 빌드하면 두개의 앱 아이콘이 생성되는 이슈가 발생됩니다.
unityLibrary를 먼저 선택한 상태에서 상단에 Build/Assemble Module 'Android.unityLibrary'를 선택합니다.
빌드가 완료되면 unityLibrary폴더 경로에 build/outputs/aar에서 unityLibrary-xxx.aar
파일이 생성된 것을 볼 수 있습니다.
Build Profiles에서 iOS로 빌드를 진행합니다.
빌드가 완료된 프로젝트를 XCode에서 열면 다음과 같이 Products에 4개의 부분으로 구성되어 있습니다
이 중에서 UnityFramework.framework
를 빌드하여 네이티브 프로젝트에 가져갈 것입니다.
그 전에 Data 폴더를 Framework에 포함시키도록 합니다.
Data 폴더를 클릭하고 Show the file Inspector/Target Membership에서 Unity- iPhone -> UnityFramework로 변경합니다.
이 과정을 통해 UnityFramework를 빌드하면 UnityFramework.framework 안에 Data 폴더가 안에 포함됩니다. (기존은 빌드하면 Unity-iPhone에 Data폴더가 포함 되는 구조.)
상단에 Scheme을 Unity-iPhone을 UnityFramework로 변경 후 재생 버튼을 클릭하여 빌드를 진행해줍니다.
빌드가 완료되면 Products폴더에서 UnityFramework가 활성화 되는 모습을 볼 수 있습니다.
Show in Finder를 클릭했을 때 UnityFramework.framework
가 존재하며 그 안에 Data 폴더가 있으면 준비 끝입니다.
android는 UI를 그리는 방식이 XML 방식과 Jetpack Compose 방식이 있습니다.
네이티브 안드로이드 프로젝트의 libs 폴더에 빌드한 aar 파일을 추가합니다. (libs폴더가 없다면 생성하면 됩니다.)
aar 파일을 마우스 오른쪽 버튼으로 클릭한 뒤, 나타나는 컨텍스트 메뉴에서 Copy Path/Reference...를 선택합니다.
이 중에서 Path from Content Root를 클릭합니다.
안드로이드 스튜디오 상단에 File/Project Structure...를 선택합니다.
Dependencies에서 Modules에 app을 선택하고 JAR/AAR Dependency를 선택합니다.
여기에 aar파일의 경로를 복사한 "libs/unityLibrary-debug.aar" 를 붙여넣고 OK 버튼을 누릅니다.
unityLibrary-debug.aar파일이 추가된 것을 확인했다면 OK를 눌러서 완료합니다.
App의 build.gradle을 보면 다음과 같이 추가된 것을 확인 할 수 있겠습니다.
요즘 유니티는 Android를 타겟팅하는 경우 UnityPlayerGameActivity
를 밀고 있는 추세입니다.
UnityPlayerGameActivity
는 com.google.androidgamesdk.GameActivity
를 상속받습니다.
이 말은 즉, 여러분들의 네이티브 안드로이드 프로젝트에 gamesdk가 필요하다는 뜻입니다.
UnityPlayerActivity를 사용하는 경우, 이 파트 내용은 생략해도 됩니다.
App의 build.gradle의 dependency에서 다음과 같이 games-activity
를 작성하고 Sync Now해줍니다.
정상적으로 임포트되었다면 다음과 같이 com.google.androidgamesdk.GameActivity
에 대해서 반응하는 모습을 볼 수 있습니다.
다음과 같이 startActivity
를 사용하면 유니티 컨텐츠를 오픈할 수 있습니다.
자바
// UnityPlayerActivity를 사용하는 경우
var intent = new Intent(this, UnityPlayerActivity.class);
startActivity(intent);
// UnityPlayerGameActivity를 사용하는 경우
var intent = new Intent(this, UnityPlayerGameActivity.class);
startActivity(intent);
코틀린
// UnityPlayerActivity를 사용하는 경우
val intent = Intent(this, UnityPlayerActivity::class.java)
startActivity(intent)
// UnityPlayerGameActivity를 사용하는 경우
val intent = Intent(this, UnityPlayerGameActivity::class.java)
startActivity(intent)
xml방식에서 다음과 같이 버튼을 추가해줍니다.
id를 부여하여 setOnClickListener
를 적용해줍시다.
다음과 같이 작성하여 유니티로 만든 것을 불러낼 수 있습니다.
xml 방식과 달리 jetpack compose라고 해서 다른 점은 없습니다. 이것 또한 동일하게 startActivity를 통해서 유니티 컨텐츠를 호출합니다.
UnityPlayerGameActivity
는 GameActivity
를 상속하고, GameActivity
는 다시 AppCompatActivity
를 상속합니다.
하지만 Jetpack Compose는 기본적으로 AppCompatActivity
를 사용하지 않습니다.
따라서 Jetpack Compose 프로젝트에서 Unity를 연동할 경우, AppCompatActivity
를 추가로 임포트해야 합니다.
계층 구조는 다음과 같습니다:
UnityPlayerGameActivity <- GameActivity <- AppCompatActivity
UnityPlayerActivity를 사용하는 경우, 이 파트 내용은 생략해도 됩니다.
App의 build.gradle.kts 파일로 이동하여 appcompat 라이브러리를 추가한 뒤 Sync Now를 클릭해 프로젝트에 반영합니다.
Sync가 완료되면 AppCompatActivity
를 정상적으로 인식할 수 있습니다.
다음과 같이 화면 중앙에 버튼을 위치시키는 간단한 UI를 구성해봅니다.
다음과 같이 작성하여 유니티로 만든 것을 불러낼 수 있습니다.
이때 context는 val context = LocalContext.current
을 작성하여 사용할 수 있습니다.
iOS에는 UI를 그리는 방식이 Storyboard 방식과 SwiftUI 방식이 있습니다.
기본적으로 iOS 앱을 타겟팅하는 프로젝트는 Supported Platforms는 iOS로 되어 있습니다.
iOS라는 뜻은 실제 기기로 빌드할 수 있고, 시뮬레이터로 테스트 해볼 수 있다는 뜻입니다.
유니티의 경우 아쉽게도 이 둘을 모두 지원할 수 없습니다.
Target SDK를 디바이스만 지원하거나, 시뮬레이터만 지원하도록 둘 중 하나를 선택해야합니다.
일반적으로 Device SDK를 선택하여 빌드를 합니다.
Xcode 프로젝트에서 PROJECT -> Build Settings로 이동하여 Supported Platforms를 변경해봅니다. 드롭다운을 클릭하여 Other...을 클릭합니다.
iphonesimulator를 선택하고 - 표시를 클릭하여 제거해줍니다.
최종적으로 Supported Platforms가 iphoneos로 단독 타겟팅 되어있는지 확인합니다.
네이티브 프로젝트에 Framework파일을 관리할 폴더를 생성합니다.
UnityFramework.framework
를 네이티브 프로젝트에 드래그 & 드롭으로 추가합니다.
네이티브 프로젝트에서 TARGETS을 선택한 후 'General' 탭으로 이동합니다.
'Frameworks, Libraries, and Embedded Content' 섹션에서 UnityFramework.framework
의 Embed 옵션을 설정을 Embed & Sign으로 지정합니다.
Embed & Sign : 프레임워크를 앱 번들에 복사하고, 개발자의 인증서로 코드 서명을 합니다.
TARGETS을 선택한 후 'Build Phases' 탭으로 이동합니다.
UnityFramework.framework
내부에 있는 Data폴더를 Copy Bundle Resources에 드래그 & 드롭으로 추가합니다.
iOS에서는 유니티 컨텐츠를 불러내는 방법이 여러가지가 있겠으나, 제일 안정적인 방법으로 소개하고자 합니다.
몇몇 포럼에서는 iOS만 유독 앱 초기에 로드가 가능한 것처럼 소개하지만 안정성이 우려되는 부분이 있습니다.
식별된 문제들 :
이러한 안정성만 해결된다면 초기에 로드해서 앱 실행 속도를 절약할 수 있다면 매우 좋을 듯 하여 위 부분이 해결되면 추가 포스팅하여 소개하도록 하겠습니다.
앱의 메인 번들 내 Frameworks
폴더에 있는 UnityFramework.framework
를 동적으로 메모리에 로드합니다.
로드된 번들에서 principalClass를 가져옵니다. 유니티의 경우 UnityFramework
라는 클래스 입니다.
principalClass.getInstance()
를 반환하여 Unity를 시작하거나, 데이터를 통신하는 등 처리를 할 수 있습니다.
private func loadUnityFramework() -> UnityFramework? {
let frameworkName = "UnityFramework.framework" // 우리가 새로 정한 이름
let bundlePath = Bundle.main.bundlePath + "/Frameworks/" + frameworkName
guard let bundle = Bundle(path: bundlePath),
let principalClass = bundle.principalClass as? UnityFramework.Type else {
print("❌ \(frameworkName) 번들을 로드하는데 실패했습니다.")
return nil
}
return principalClass.getInstance()
}
이전에 설명한 loadUnityFramework()
함수를 호출하여, 앱에 내장된 Unity 프레임워크를 메모리에 로드하고 그 인스턴스를 가져옵니다. self는 이 showUnity 함수를 포함하고 있는 클래스의 인스턴스를 의미합니다.
이것을 Unity 프레임워크에 등록(register)함으로써, Unity 쪽에서 발생하는 특정 이벤트나 메시지를 네이티브 iOS 코드에서 받을 수 있습니다. (예 : SendMessage)
runEmbedded(withArgc, argv, appLaunchOpts)
를 호출함으로써 Unity 런타임이 초기화되고, 네이티브 iOS 앱의 화면 위에 Unity 컨텐츠가 렌더링됩니다.
public func showUnity() {
framework = loadUnityFramework()
framework?.register(self) // Unity로부터 콜백(ex: unityDidLaunch)을 받기 위해 등록
framework?.runEmbedded(withArgc: CommandLine.argc, argv: CommandLine.unsafeArgv, appLaunchOpts: nil)
}
위에서 소개한 내용들을 토대로 UnityManager.swift라는 싱글턴 클래스를 프로젝트에 추가하여 손쉽게 유니티 컨텐츠를 로드하도록 해봅니다.
import Foundation
import UnityFramework
public class UnityManager: NSObject, UnityFrameworkListener {
public static let shared = UnityManager()
private var framework: UnityFramework?
/// 유니티 컨텐츠를 동작시킵니다.
public func showUnity() {
framework = loadUnityFramework()
framework?.register(self) // Unity로부터 콜백(ex: unityDidLaunch)을 받기 위해 등록
framework?.runEmbedded(withArgc: CommandLine.argc, argv: CommandLine.unsafeArgv, appLaunchOpts: nil)
}
private func loadUnityFramework() -> UnityFramework? {
let frameworkName = "UnityFramework.framework" // 우리가 새로 정한 이름
let bundlePath = Bundle.main.bundlePath + "/Frameworks/" + frameworkName
guard let bundle = Bundle(path: bundlePath),
let principalClass = bundle.principalClass as? UnityFramework.Type else {
print("❌ \(frameworkName) 번들을 로드하는데 실패했습니다.")
return nil
}
return principalClass.getInstance()
}
}
Main.storyboard에 버튼을 추가하고 버튼 클릭 이벤트를 구성해봅니다.
이 버튼을 터치했을 때 우리가 만든 Unity 콘텐츠가 실행되도록, 버튼의 동작(action) 부분에 UnityManager.shared.showUnity()
코드를 추가해 주기만 하면 됩니다.
ContentView에 버튼을 추가하고 버튼 클릭 이벤트를 구성 해봅니다.
이 버튼을 터치했을 때 우리가 만든 Unity 콘텐츠가 실행되도록, 버튼의 동작(action) 부분에 UnityManager.shared.showUnity()
코드를 추가해 주기만 하면 됩니다.
자료 잘 없는 분야 직접 파보시면서 테스트해보시는 모습 멋지다고 생각합니다