Android XR

Meningkatkan pengalaman imersif dengan Mesin 3D di Jetpack Compose untuk XR

Speaker

Tentang

Tentang konferensi

Pelajari cara mengintegrasikan game engine 3D (Godot & Unity) dengan Jetpack Compose ke dalam satu codebase untuk perangkat seluler dan Android XR. Kita akan mempelajari cara menyematkan adegan 3D ke dalam penampung spasial, menyiapkan komunikasi Kotlin ke mesin game, serta melihat secara eksklusif cara NBA membangun pengalaman basket live di Android XR.

Ringkasan

  • Ekstensi OpenXR Spatial Containers memungkinkan mesin 3D berbagi ruang dengan UI Android native. Alih-alih mengambil alih skybox, mesin real-time berjalan di dalam volume 3D terkelola bersama panel spasial Jetpack XR.
  • Developer dapat menyematkan mesin real-time langsung ke dalam aplikasi Jetpack Compose sebagai library AAR. Pendekatan ini memungkinkan Android native menangani logika bisnis dan UI, sementara mesin merender komponen 3D.
  • Menyematkan Godot memerlukan dependensi AAR dan aturan build AAPT untuk menyimpan aset tersembunyi. Subclass GodotPlugin kemudian menangani komunikasi dua arah dengan Kotlin.
  • Loop UI Compose harus tetap dipisahkan dari loop tick mesin berkelanjutan. Meneruskan update mesin secara membabi buta akan memicu rekomposisi terus-menerus dan menyebabkan jeda UI.
  • Aplikasi NBA mengintegrasikan Unity sebagai library tersemat dalam proses terpisah menggunakan AIDL untuk komunikasi antarproses, sehingga melindungi aplikasi host dari error.

Resource

Gemini Notebook

Topik

  • android-xr
  • jetpack-compose
  • spatial-containers
  • openxr
  • godot
  • kesatuan

Transkrip

Pengantar Android XR

  • Halo semuanya.

  • Nama saya Luke Hopkins.

  • Saya adalah engineer developer relations di tim Android XR.

  • Dalam pembahasan hari ini, kita akan membahas cara menyematkan game engine, seperti Godot dan Unity, ke dalam project Android Anda.

  • Nanti, saya akan bergabung dengan Josh dan Rodrigo dari NBA dan Beyond Sports, yang akan membahas cara mereka menerapkan Unity sebagai library ke dalam aplikasi NBA mereka.

  • Dalam sesi ini, kita akan membahas cara mengintegrasikan Godot ke dalam aplikasi Jetpack Compose, lalu Josh dan Rodrigo akan membahas aplikasi NBA mereka.

  • Android XR adalah ekstensi ekosistem Android, yang memperluas berbagai faktor bentuk untuk menghadirkan pengalaman yang Anda kenal dan sukai ke dimensi baru.

  • Kami telah membuat proses pemindahan aplikasi XR dan aplikasi Android yang ada ke ruang baru ini menjadi intuitif, dan kami memanfaatkan teknologi Android yang sudah ada yang Anda gunakan saat ini.

  • Android XR adalah platform terpadu yang dirancang untuk berbagai perangkat.

  • Perangkat Android XR mendukung berbagai jenis pengalaman.

  • Kami memiliki pengalaman augmented untuk audio dan Kacamata Pintar dengan Layar, lalu kami memiliki pengalaman imersif untuk headset XR dan kacamata XR berkabel.

  • Hari ini, kita akan membahas cara membangun pengalaman imersif untuk kacamata XR berkabel dan headset XR.

  • Pembuatan pengalaman imersif dapat dilakukan melalui Jetpack XR SDK atau menggunakan OpenXR dengan mesin real-time.

  • Kami mendukung Unity, Godot, dan Unreal sehingga Anda dapat membangun pengalaman imersif untuk Android, apa pun alat yang Anda gunakan.

  • Dalam pembahasan ini, kami akan menjelaskan cara memanfaatkan SceneCore API Jetpack, bersama dengan Godot dan ekstensi OpenXR baru, untuk mewujudkannya.

  • Di sini, Anda dapat melihat Godot berjalan berdampingan dengan panel spasial Jetpack yang berjalan secara bersamaan.

  • Perhatikan bagaimana Godot dirender sebagai konten spasial, yang menyediakan volume 3D bersama UI Compose 2D dalam penampung spasial.

  • Pendekatan ini memungkinkan developer menjalankan simulasi fisika, shader kustom menggunakan mesin real-time pilihan mereka, sekaligus menerapkan praktik pengembangan Android native ke bagian lain aplikasi Anda.

  • Dengan Android XR dan XREAL AURA, Anda juga dapat menciptakan pengalaman seperti ini.

  • Dalam presentasi ini, kami akan mengajari Anda cara membangun pengalaman ini menggunakan Godot dan Android Studio, lalu Anda akan mendengar dari partner kami di NBA dan Beyond Sports tentang cara mereka membangunnya menggunakan Unity sebagai library.

Penampung spasial

  • Secara historis, membangun pengalaman XR dengan OpenXR berarti membangun pengalaman yang merender setiap piksel.

  • Artinya, jika developer perlu menampilkan aktivitas kedua, mereka tidak dapat menampilkan keduanya sekaligus.

  • Untuk mengatasi hal ini, Khronos, kelompok kerja OpenXR, bekerja sama dengan beberapa pemimpin industri, telah merilis ekstensi OpenXR Spatial Containers.

  • Anggap Container Spasial sebagai OpenXR dalam kotak.

  • Alih-alih mesin Anda memiliki seluruh skybox, mesin tersebut diberi volume 3D yang dikelola sistem yang dapat berdampingan dengan panel spasial XR Jetpack, Penampung Spasial lainnya, dan aktivitas Android standar.

  • Misalnya, Anda ingin memanfaatkan Android XR dengan menampilkan dua panel spasial sekaligus, sambil menjalankan game imersif.

  • Anda merender semuanya di mesin, dan hal ini memungkinkan editor Godot dan penampung spasial berjalan secara bersamaan dalam kasus penggunaan ini.

  • Mulai level API Spasial 4, Google telah menerapkan Penampung Spasial ke Android XR.

  • Penampung Spasial mendukung dua mode tampilan penting.

  • Pertama adalah mode terbatas.

  • Hal ini memberikan volume 3D yang ditentukan dengan beberapa penghindaran bawaan.

  • Pengguna dapat mengambil penampung spasial, memutarnya, memosisikannya, mengambilnya, meminimalkannya, dan penampung ini mendukung proyeksi kedalaman stereo, sehingga memberi Anda jendela ajaib atau efek pop-out 3D yang Anda miliki sekaligus memotong tepi penampung.

  • Yang kedua adalah mode tidak terbatas.

  • Jika pengguna memutuskan ingin mendapatkan pengalaman imersif penuh, aplikasi Anda dapat meminta transisi yang lancar dari mode terbatas ke mode ruang penuh, lalu Anda juga dapat kembali ke mode terbatas.

  • Dengan penambahan Penampung Spasial, Android XR membuka peluang untuk ide-ide baru dan luas.

  • Misalnya, Anda ingin menghadirkan UI Android ke dalam game, atau mungkin Anda ingin membuat shader kustom untuk menampilkan konten 3D di aplikasi Anda.

  • Sebelum pengenalan Spatial Containers, Compose dan game engine tidak dapat dijalankan secara bersamaan.

  • Dan saat melihat pengembangan aplikasi di mesin real-time, kita juga telah hidup dalam dua paradigma yang berbeda.

  • Bagi developer Android native, runtime aplikasi Anda dibuat berdasarkan Jetpack Compose.

  • Alat ini dirancang untuk UI yang cepat dan reaktif, efisiensi baterai, tata letak dinamis, aksesibilitas sistem, dan input teks yang lancar.

  • Sistem ini tidak aktif saat tidak ada aktivitas, dan dapat diprediksi.

  • Bagi developer game engine, domainnya adalah rendering real-time, yang merupakan keunggulan alat seperti Godot dan Unity.

  • Mereka tidak hanya menunggu interaksi pengguna.

  • Game menjalankan loop tick dan render berkelanjutan.

  • Jadi, kecepatan frame-nya bisa 60, 90, 100 frame per detik.

  • Mesin game melakukan penghitungan fisika, shader kompleks, animasi skeletal, audio spasial, dan semua ini dengan performa yang luar biasa.

Pendekatan untuk mengintegrasikan mesin 3D

  • Jadi, apa yang terjadi jika Anda menginginkan yang terbaik dari kedua dunia?

  • Sebelumnya, developer harus menerapkan salah satu dari dua pendekatan ini.

  • Opsi A adalah menulis ulang seluruh aplikasi Anda di dalam mesin sungguhan seperti Godot atau Unity.

  • Membuang codebase yang ada berarti kehilangan banyak UI Compose native, menerapkan ulang teks dan navigasi dasar, serta menulis wrapper di sekitar Android API seperti text-to-speech.

  • Opsi B adalah membangun pipeline rendering grafis 3D tingkat rendah dari awal di dalam aplikasi native Anda menggunakan API Vulkan dan OpenXR mentah.

  • Alih-alih menghadirkan fitur produk, Anda pada dasarnya membangun mesin eksklusif Anda sendiri.

  • Jadi, hari ini saya dapat menunjukkan cara memanfaatkan Godot sebagai library dan Unity sebagai library.

  • Dengan penambahan penampung spasial ini, Anda kini dapat memperoleh manfaat dari kedua dunia tersebut.

  • Kita dapat melakukannya dengan menyematkan mesin real-time langsung di dalam library di dalam aplikasi Jetpack Anda.

  • Anda mempertahankan struktur aplikasi Android produksi, logika bisnis, dan rendering konten spasial 3D yang lancar di dalam aplikasi Anda.

  • Hal ini juga berarti Anda dapat memiliki satu APK untuk semua faktor bentuk.

  • Jika Anda tidak hanya ingin menampilkan konten 3D, Anda dapat memilih untuk tidak memuat library ke dalam aktivitas atau fragmen dan menjaga codebase tetap jelas, serta memungkinkan Anda mendukung banyak perangkat.

  • Filosofinya sederhana, pisahkan aplikasi dan jadikan mesin real-time sebagai library.

  • Alih-alih memilih antara aplikasi Android atau engine, Anda memperlakukan engine sebagai library software tersemat, yang juga dikenal sebagai dependensi AAR.

  • Anda dapat menariknya langsung ke build Gradle, seperti library lainnya.

  • Kode Android native Anda tetap menjadi otak utama aplikasi yang menghosting komponen 3D.

  • Jetpack Compose XR menangani hal yang paling dikuasainya, yaitu merender UI native yang lancar dan mudah diakses serta mengelola status aplikasi.

  • Dan saat elemen 3D diperlukan, aplikasi akan menghosting tampilan rendering tersemat yang dirender oleh game engine itu sendiri.

  • Hari ini, kita akan membahas secara mendalam cara kerja fitur ini di balik layar untuk Godot.

  • Saya akan mulai dengan mekanisme penyematan dan pembuatan komunikasi dua arah antara Kotlin dan GDScript.

  • Kemudian, kita akan melihat ekstensi OpenXR baru, Spatial Container, yang akan benar-benar membuka generasi baru multitasking dan tata letak spasial.

  • Mari kita lihat sekilas demo yang saya tunjukkan sebelumnya.

  • Saya membuatnya menggunakan Godot dan Android Studio.

  • Klip ini menunjukkan Jetpack Compose untuk XR yang merender panel spasial UI 2D native untuk papan skor, notifikasi, dan sorotan.

  • Selain itu, Godot merender simulasi pertandingan sepak bola.

Komunikasi antara Kotlin dan Godot

  • Di sini, Anda dapat melihat perincian cara kami berkomunikasi dari Kotlin ke GDScript.

  • Untuk melakukannya, Godot menyediakan arsitektur plugin.

  • Anda menulis class Kotlin ringan yang memperluas GodotPlugin.

  • Class ini berfungsi sebagai antarmuka antara JVM dan runtime C++ Godot.

  • Oleh karena itu, interaksi antara aplikasi Anda dan Godot sangatlah ringan.

  • Arsitektur Godot mendukung komunikasi dua arah antara Godot dan Kotlin.

  • Saat peristiwa game terjadi di Godot, seperti gol atau pelanggaran, Godot dapat langsung memanggil fungsi Kotlin dan memperbarui UI Compose secara real time.

  • Kemudian, saat pengguna berinteraksi dengan pemutar media, misalnya, untuk menjeda atau memilih sorotan yang berbeda, Kotlin akan memancarkan sinyal kembali ke Godot, dan tindakan game engine yang sesuai akan terjadi.

  • Setelah plugin Godot dilintasi, performa dan pengelolaan akan berada di sepanjang garis domain.

  • Terapkan praktik standar terbaik untuk aplikasi Android dan praktik standar terbaik untuk project Godot.

  • Sekarang kita akan melihat arsitekturnya.

  • Mari kita lihat cara menghubungkan ini ke Godot dalam aplikasi Android.

  • Runtime mesin Godot didistribusikan melalui Maven sebagai AAR.

  • Mengintegrasikan ke dalam build Anda cukup mengikuti praktik Android standar.

  • Ada satu langkah penting yang harus segera diperhatikan, dan itulah yang saya sebut sebagai jebakan aset.

    androidResources {
         ignoreAssetsPattern = "!.svn:!.git:!.gitignore:!.ds_store:!*.scc:<dir>_*:!CVS:!*~:!android:!export_presets.cfg"
     }
  • Dalam project Godot, scene dan file konfigurasi project Anda berada di tempat yang sama.

  • Saat menyematkan Godot, project Godot ditambahkan langsung di dalam folder aset Anda.

  • Artinya, semua mesh 3D, tekstur terkompresi, dan tekstur yang dikompilasi yang diimpor akan disimpan sebagai artefak runtime di dalam file tersembunyi bernama .godot.

  • Secara default, Android akan menghapus folder ini saat melakukan build, sehingga mengabaikan direktori apa pun yang memiliki titik.

  • Jadi, jika Anda menyinkronkan dan mem-build aplikasi seperti biasa, APK akan di-build, tetapi akan error dan dimuat karena hal-hal tersebut belum dikompilasi ke dalam APK.

  • Jadi, untuk memperbaikinya, Anda cukup mengganti opsi AAPT secara eksplisit di dalam build.gradle dan mengizinkan titik awal terjadi dari Godot.

  • Setelah itu, Anda dapat menjalankan scene Godot sekali di editor Godot, dan dari sana Anda dapat membangun semua hal lainnya dari Android Studio.

  • Untuk mendukung penampung spasial dan OpenXR terbaru, sebaiknya gunakan Godot 4.8 dan yang lebih baru, ekstensi vendor Godot 5.2, dan loader OpenXR Khronos 1.1.63.

    dependencies {
        // Godot 4.8 Android Library + Godot OpenXR Vendors plugin + Khronos OpenXR Loader
        implementation("org.godotengine:godot:4.8.0.dev-SNAPSHOT")
        implementation("org.godotengine:godot-openxr-vendors-androidxr:5.1.0-stable")
        implementation("org.khronos.openxr:openxr_loader_for_android:1.1.63")
    
        // Jetpack XR SDK (Compose for XR, SceneCore, Runtime)
        implementation("androidx.xr.compose:compose:1.0.0-beta02")
        implementation("androidx.xr.scenecore:scenecore:1.0.0")
        implementation("androidx.xr.runtime:runtime:1.0.0")

Merender Godot di Jetpack Compose

  • Setelah aset dipaketkan, bagaimana Godot dan Compose merendernya?

  • Library Godot menyediakan metode factory yang disebut onInitRenderView.

    @Composable
    fun GodotView(godotHost: GodotHost, modifier: Modifier = Modifier) {
        // 1. Initialize Godot's native render surface
        val renderView = remember(godotHost) {
            godotHost.godot!!.onInitRenderView(godotHost)
        }
        // 2. Host the SurfaceView inside Jetpack Compose
        AndroidView(
            modifier = modifier,
            factory = { renderView }
        )
    }
  • Tindakan ini akan menampilkan SurfaceView yang baru saja dirender oleh Godot.

  • Jadi, Anda menggabungkannya dalam fungsi Composable, dan Godot kini dapat diubah ukurannya, diberi padding, dan ditempatkan di mana saja di dalam panel entity.

    Subspace {
            SpatialPanel(
                modifier = SubspaceModifier
                    .width(960.dp)
                    .height(540.dp)
                    .offset(z = (-200).dp)
                    .movable()
    ,
        ) {
            GodotView(
                godotHost = godotHost,
                modifier = Modifier.fillMaxSize(),
            )
        }
    }
  • Mesin itu sendiri tidak dapat mengelola siklus proses.

    @Composable
    fun GodotView(
       godotHost: GodotHost,
       modifier: Modifier = Modifier,
       lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    ) {
    
       DisposableEffect(lifecycleOwner, godotHost) {
           val observer = LifecycleEventObserver { _, event ->
               val engine = godotHost.godot ?: return@LifecycleEventObserver
               when (event) {
                   Lifecycle.Event.ON_START -> engine.onStart(godotHost)
    ….
                   Lifecycle.Event.ON_DESTROY -> engine.onDestroy(godotHost)
                   else -> {}
               }
           }
    
           lifecycleOwner.lifecycle.addObserver(observer)
    
           onDispose {
               lifecycleOwner.lifecycle.removeObserver(observer)
           }
       }
    
    }
  • Jadi, aktivitas Anda perlu mengimplementasikan antarmuka GodotHost.

  • Hal ini akan memastikan bahwa setiap siklus proses yang terjadi dalam aktivitas Anda, saat dijeda, saat dilanjutkan, semuanya akan dikirim ke mesin Godot itu sendiri, dan sekarang mesin tersebut berbasis siklus proses.

  • Selanjutnya, kita perlu menambahkan aplikasi native dan mesin kita untuk berkomunikasi dengan lancar.

  • Jadi, untuk melakukannya, kita dapat menggunakan antarmuka GodotHost.

    class MainActivity : ComponentActivity(), GodotHost {
        override fun getHostPlugins(engine: Godot): Set<GodotPlugin> =
            setOf(SoccerGodotPlugin(engine))
    }
  • Kita dapat menentukan plugin Godot, dan di sini kita dapat memastikan bahwa saat Godot dimulai, plugin tersebut didaftarkan dari sisi Compose.

  • Sekarang, setelah plugin terdaftar, mari kita siapkan Android untuk berkomunikasi dengan memancarkan sinyal ke Godot.

  • Dalam kode Kotlin, kita akan membuat instance GodotPlugin.

  • Kita akan meminta aktivitas kita mendaftarkan plugin ke library Godot.

  • Kemudian di Kotlin, kita membuat GodotPlugin, mendaftarkan namanya, dan memberi tahu Godot semua sinyal yang ingin kita gunakan dalam paket ini.

    class SoccerGodotPlugin(godot: Godot) : GodotPlugin(godot) {
        override fun getPluginName() = "SoccerPlugin"
        // 1. Signals sent from Android Compose UI -> Godot
        override fun getPluginSignals() = setOf(
            SignalInfo("camera_mode_changed", String::class.java),
            SignalInfo("match_command", String::class.java),
        )
    
    }
  • Selanjutnya, kita memiliki Skrip Godot, tempat Anda bisa mendapatkan instance plugin ini.

    var soccer_plugin: Object = null
    
    func _ready() -> void:
        if Engine.has_singleton("SoccerPlugin"):
            soccer_plugin = Engine.get_singleton("SoccerPlugin")
    
            # Listen for signals from Jetpack Compose UI
            soccer_plugin.connect("camera_mode_changed", _on_camera_mode_changed)
            soccer_plugin.connect("match_command", _on_match_command)
  • Kita dapat melihat apa yang terdaftar, lalu di sini kita dapat menghubungkan fungsi ke sinyal yang telah kita daftarkan.

  • Jadi, setelah Anda memanggil sesuatu seperti emitSignal, Anda dapat meneruskannya dari kode Kotlin, dan kode tersebut akan langsung masuk ke Godot.

    class SoccerGodotPlugin(godot: Godot) : GodotPlugin(godot) {
    
        fun sendCommandToGodot(command: String) {
            emitSignal("match_command", command)
        }
    }
  • Agar Godot dapat berkomunikasi dengan Kotlin, misalnya saat terjadi gol, kita dapat memanggil fungsi Kotlin langsung dari Skrip Godot, cukup dengan menggunakan instance plugin.

    func _on_goal_scored(minute: int, home: int, away: int) -> void:
    
        if soccer_plugin:
            soccer_plugin.onMatchEvent("GOAL", minute, home, away)
  • Jadi, pertama-tama, Anda menganotasi fungsi Kotlin dengan UsedByGodot.

    @UsedByGodot
    fun onMatchEvent(type: String, minute: Int, home: Int, away: Int, title: String) {
    
            _uiState.update { state ->
                state.copy(homeScore = home, awayScore = away, matchSeconds = minute)
            }
        }
  • Mesin menangani marshalling JNI di balik layar.

  • Kemudian, kita dapat memanggil fungsi tersebut langsung dari Godot Script.

  • Ada satu tips singkat yang perlu diingat saat menggunakan fitur ini, yaitu UsedByGodot memerlukan pencocokan string yang sama persis.

  • Jadi, jika Skrip Godot Anda memanggil onMatchEvent, nama Kotlin Anda harus sama persis, sehingga tidak ada konversi camel case atau snake-case otomatis.

  • Jadi, Anda hanya perlu memastikan kedua metode tersebut sama persis.

  • Setelah plugin terhubung, bagian paling penting dari pembahasan ini adalah apa yang saya sebut sebagai model mental.

  • Di sinilah game engine dan Jetpack Compose beroperasi pada dua arsitektur yang pada dasarnya berlawanan.

  • Game engine beroperasi pada tick frame berkelanjutan.

  • Baik Anda mengubah sesuatu atau tidak, proses tick game engine terjadi 60, 90, 100 kali per detik, sehingga menghasilkan rendering real-time yang kaya dan lancar.

  • Dan mesin seperti Godot menggunakan siklus GPU dan CPU secara efektif.

  • Di sisi lain, Jetpack Compose menangani berbagai hal secara reaktif dan berbasis status.

  • Dan saat aplikasi tidak berubah, Compose akan terus berjalan, menggunakan 0 CPU.

  • Jika Anda ingin menghubungkan keduanya untuk menjalankan dua runtime tanpa strategi yang jelas, filosofi ini cenderung bertabrakan.

  • Jadi, mari kita lihat bagaimana kita mengatasi masalah ini dengan apa yang saya sebut sebagai kebocoran tick.

  • Di sinilah kita memiliki pengalaman 3D, misalnya timer papan skor.

  • Dalam skrip mesin, ada variabel timer yang terus berubah.

  • Dan jika Anda meneruskan variabel ini secara membabi buta ke Compose, misalnya 90 kali per detik, Anda akan memasuki loop rekomposisi ini 90 kali per detik.

  • Jadi, ini berarti Compose akan mengevaluasi ulang Composable Anda, mengukur ulang tata letak, dan menggambar ulang hierarki UI 90 kali per detik.

  • Anda akan melihat penurunan frame, jeda UI, dan mengalami throttling termal dengan cepat.

  • Jadi, aturan utamanya adalah jangan pernah membiarkan loop mesin berkelanjutan mendikte loop UI reaktif.

  • Dalam contoh ini, daripada meminta Godot memperbarui timer papan skor secara terus-menerus, saya hanya memastikan Android mengambil data ini sekali setiap detik saat diperlukan.

    LaunchedEffect(uiState.phase) {
        while (uiState.phase == MatchPhase.PLAYING) {
            delay(1000L) // Update scoreboard timer at 1 Hz, not 90 Hz
            SoccerGodotPlugin.tickLocalClock()
        }
    }
    ...
    @UsedByGodot
    fun onMatchEvent(eventTypeStr: String, matchMinute: Int, ...) {
        // Recompose once when threshold is crossed to display the popup banner
        _uiState.update { it.copy(activeBannerEvent = event, ...) }
    }
  • Terakhir, selalu ingat batas thread.

  • Loop game engine Godot terus dieksekusi di thread rendernya sendiri.

  • UI, UI Android harus dieksekusi di thread utama Android.

  • Jadi, selalu pastikan plugin Kotlin Anda mengirimkan peristiwa menggunakan Dispatchers.Main atau primitif status aman sebelum memperbarui status model tampilan Anda.

    private val mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
    mainScope.launch {
       _uiState.update { current ->
            current.copy(
               homeScore = homeScore,
               awayScore = awayScore,
               matchSeconds = matchMinute.coerceIn(0, 90),
               phase = updatedPhase,
               events = newEventsList,
               activeBannerEvent = event
           )
       }
  • Dengan mempertahankan pekerjaan frekuensi tinggi di mesin dan semantik diskret di Compose, Anda bisa mendapatkan area pandang 3D yang sangat lancar tanpa mengorbankan masa pakai baterai, aksesibilitas, atau performa aplikasi native Anda.

  • Sekarang, mari kita lihat cara menambahkan Spatial Container ke project Godot Anda.

  • Pertama, mari kita mulai dengan menyiapkan aplikasi agar menjadi Ruang Penuh, mengaktifkan Penampung Spasial dengan petunjuk pembatasnya, lalu menetapkan library loader XR di file Manifes Android.

    <uses-native-library
               android:name="libopenxr.google.so"
               android:required="false" />
    
               <!-- 1. Launch Activity in Full-Space Managed XR mode -->
               <property
                   android:name="android.window.PROPERTY_XR_ACTIVITY_START_MODE"
                   android:value="XR_ACTIVITY_START_MODE_FULL_SPACE_MANAGED" />
    
               <!-- 2. OpenXR Spatial Container bounds mode hints -->
               <property
                   android:name="org.khronos.openxr.PROPERTY_ACTIVITY_HINT_XR_SPATIAL_CONTAINER_USED_BOUNDS_MODES"
                   android:value="" />
  • Godot memiliki setelan project XR-nya sendiri, tempat saya dapat mengaktifkan OpenXR, shader XR, dan Penampung Spasial, hanya melalui editornya.

  • Dan hanya itu yang perlu Anda lakukan untuk mengaktifkan Spatial Containers dalam Godot.

  • Jadi, setelah Anda berhasil membuat Godot berfungsi dengan Android XR, Penampung Spasial, dan semua hal yang telah dikembangkan ini, praktik terbaik untuk menggunakan Penampung Spasial, serta panel spasial dan JXR, kami sangat merekomendasikan untuk melihat informasi selengkapnya di situs developer.

  • Kita baru saja membahas contoh cara menggunakan Spatial Container dalam Godot Engine dan Android XR, dan kami yakin banyak dari Anda yang ingin mendengar pendapat dari developer yang aktif mengerjakan Spatial Container untuk pengalaman Android XR mereka.

  • Jadi, selanjutnya, saya ingin mengundang Josh dan Rodrigo dari NBA, yang merupakan salah satu partner pertama yang menggunakan Spatial Container dalam aplikasi mereka.

Ringkasan NBA Court View

  • Terima kasih, Luke.

  • Halo semuanya.

  • Nama saya Josh, dan saya adalah developer Android di NBA.

  • Saya di sini bersama rekan saya, Rodrigo Sanchez, developer software Unity di Beyond Sports.

  • Hari ini, kami ingin menunjukkan cara kami menggunakan Unity as a Library untuk membuat Court View untuk aplikasi NBA.

  • Di NBA, kami selalu berupaya menghadirkan pengalaman baru bagi para penggemar.

  • Jadi, bersama Google dan Beyond Sports, kami dengan senang hati memperkenalkan Court View.

  • Court View menawarkan reka ulang 3D pertandingan NBA, sehingga pelanggan League Pass dapat menonton dari perspektif baru yang tidak mungkin dilakukan dengan video siaran biasa.

  • Skalakan dan putar ulang lapangan, tempatkan di mana pun Anda suka di ruang Anda.

  • Bergeraklah di sekitarnya, dan tonton pertandingan dari sudut yang unik.

  • Sementara itu, Anda dapat menggunakan aplikasi NBA seperti biasa, mulai dari melihat statistik sinkronisasi hingga menonton siaran langsung.

  • Bagaimana cara menyiapkan semua ini?

  • Court View dirender di Unity dan digunakan oleh aplikasi Android kami menggunakan Unity as a Library.

  • Kemudian, kami membuat satu Android Archive, yang dihosting di Maven Central, yang dapat ditarik oleh aplikasi NBA sebagai satu dependensi.

  • Dengan begitu, kami dapat membagi pekerjaan antara sisi Android dan sisi Unity.

  • Jadi, kita masing-masing dapat berfokus pada hal yang paling kita ketahui, dan bekerja secara paralel untuk mempercepat pengembangan secara keseluruhan.

  • Sebagai contoh, saat tim kami mengerjakan pengalaman ruang penuh di Compose, tim Unity dapat berfokus pada penskalaan dan tata letak lapangan 3D.

  • Sekarang, saya akan menyerahkan kepada Rodrigo untuk membahas lebih lanjut project Unity dan cara mereka bekerja dengan Unity as a Library.

Mengintegrasikan Unity sebagai library

  • Terima kasih, Josh.

  • Kami di Beyond Sports telah bekerja sama dengan NBA selama beberapa tahun untuk merepresentasikan visualisasi 3D dengan data nyata dari pertandingan.

  • Dengan project ini, kami menyesuaikan simulasi 3D kami agar kompatibel dengan Android XR.

  • Simulasi kami dibangun di mesin Unity dengan C#.

  • Kami mengekspor simulasi ke Android dengan Unity as a Library, dan membangun lapisan komunikasi di sampingnya menggunakan Kotlin.

  • Sebelum menggabungkan kedua modul ini menjadi satu dependensi Maven untuk disertakan NBA dalam aplikasinya.

  • Di layar, Anda dapat melihat video singkat simulasi yang sama dengan yang ditunjukkan Josh sebelumnya, yang berjalan di editor.

  • Untuk mencapai hal ini, kita menggunakan paket Android XR Extensions untuk Unity.

  • Mari kita pelajari prosesnya.

  • Jadi, sebelum membangun di Unity, pastikan Anda telah memilih setelan dan plugin yang tepat.

  • Dapatkan paket XR Plug-in Management Unity dan paket Android XR Extensions Google untuk Unity.

  • Konfigurasikan di setelan XR Plug-in Management untuk menggunakan penyedia OpenXR Android XR Extensions.

  • Untuk kasus penggunaan kami, kami ingin menggunakan Spatial Containers, jadi kami mengaktifkan fitur tersebut dan menonaktifkan XR imersif dari setelan pengelolaan sesi.

  • Kami tidak menggunakan paket input XR, karena semua input ditangani secara native, di luar Unity.

  • Selanjutnya, di setelan pemutar, Anda harus menyetel project untuk menggunakan level API minimum 24 atau yang lebih tinggi, dan titik entri aplikasi harus disetel ke Game Activity yang lebih baru.

  • Perhatikan bahwa Activity tidak boleh dicentang.

  • Kita hanya tertarik untuk membangun ARM64, jadi kita memilih arsitektur target tersebut, dan di bagian resolusi, kita mengaktifkan Run in Background, sehingga Anda dapat melakukan multitasking dengan aplikasi lain, dan tetap menjalankan simulasi 3D di sampingnya.

  • Kami juga mengaktifkan Aktivitas yang Dapat Diubah Ukurannya, karena diperlukan untuk merender dialog, seperti permintaan izin sistem.

  • Terakhir, sebaiknya aplikasi Android XR dirender menggunakan Universal Render Pipeline dan Vulkan Graphics API.

  • Di aset URP, HDR dan pascapemrosesan harus dinonaktifkan.

  • Setelah menyelesaikan semua langkah tersebut, Anda siap membuat build.

  • Buka jendela build, dan jika Anda menggunakan Unity 6.1 atau yang lebih baru, Anda dapat memilih profil build Android XR, yang memungkinkan Anda memisahkan setelan jika Anda juga mem-build untuk Android Mobile.

  • Sebelum membangun, pastikan untuk mengaktifkan project ekspor.

  • Setelah Unity selesai mem-build, Anda akan memiliki project Android yang berfungsi dan dapat dirakit di Android Studio serta dijalankan di perangkat seperti APK Unity lainnya.

  • Setelah berada di Android Studio, Anda perlu mengubah build yang diekspor menjadi library untuk digunakan oleh aplikasi lain.

    // The Unity build
    include(":unityLibrary")
    project(":unityLibrary").projectDir = File("..\\Libraries\\unityBuild\\unityLibrary")
    
    // Custom communication IPC module
    include(":nativeCommunications")
    project(":nativeCommunications").projectDir = File("..\\Libraries\\nativeCommunications")
    
    // If using XR features
    include(":unityLibrary:xrmanifest.androidlib")
    
    // Used for merging libraries, exporting and publishing.
    include(":packageExporter")
    
    dependencyResolutionManagement {
       repositories {
    flatDir { // Required to load unity correctly
       dirs("${project(":unityLibrary").projectDir}/libs")
    }
    }
  • Dalam project ini, kami memiliki empat modul berbeda: modul APK utama untuk diuji sebagai placeholder tiruan untuk aplikasi wrapper NBA, modul Unity Library yang menyimpan build yang diekspor, modul nativeCommunications yang menyimpan lapisan komunikasi yang kami bangun di antara aktivitas, dan modul packageExporter, yang kami gunakan untuk menggabungkan AAR ke dalam satu paket untuk dikirim ke Maven.

  • Penggabungan beberapa AAR dapat dilakukan menggunakan Android Fused Library, atau metode komunitas yang lebih lama seperti Fat AAR.

  • Anda dapat menjaga struktur dan komunikasi tetap sederhana dengan menjaga Unity dan aktivitas utama berjalan dalam proses yang sama.

  • Kami memutuskan untuk tidak melakukannya, dan memilih untuk memisahkan aktivitas Unity ke dalam prosesnya sendiri sebagai pengaman jika bagian Unity mengalami error, sehingga aplikasi NBA tidak terpengaruh dan dapat menangani penutupan Unity dengan aman tanpa mengalami error.

  • Keputusan ini memaksa kami untuk mengadopsi sistem komunikasi antar-proses yang lebih kuat.

  • Karena aktivitas berada dalam proses yang berbeda, tidak ada memori yang dibagikan di antara aktivitas tersebut, dan banyak metode IPC Android umum tidak berfungsi atau tidak memenuhi persyaratan.

  • Kami menggunakan Android Interface Definition Language, atau AIDL, untuk berkomunikasi antara aktivitas utama NBA dan aktivitas Unity tersemat kami.

  • Kemudian, komunikasi antara Android dan Unity juga didasarkan pada antarmuka, menggunakan AndroidJavaProxy bawaan di Unity untuk mendaftarkan antarmuka Kotlin ke implementasi antarmuka C#.

  • Modul komunikasi kami terutama berfokus pada apa yang kami sebut sebagai jembatan aktivitas.

    // Main Activity
    
    val intent = Intent(context, AIDLBridgeService::class.java)
    context.startService(intent)
    context.bindService(intent, connection, 0)
    
    fun onStartMatchClicked() {
       bridge.runWithService { service ->
           service.startMatch("match_id")
       }
    }
    
    // AIDL Bridge Service
    
    private val remoteBinder = object : IBridgeService.Stub() {
    override fun startMatch(matchID: String?) {
       addToCallbackQueue { nativeInterface?.startMatch(matchID) }
    }
    }
  • Kedua aktivitas memiliki akses ke jembatan aktivitas dan metode utilitas di dalamnya.

  • Bridge berisi referensi ke layanan yang mengimplementasikan antarmuka AIDL yang kita tentukan, dan juga menyediakan cara mudah bagi aktivitas untuk mengikat ke proses, maaf, ke layanan dan mengelola siklus prosesnya secara internal.

  • Memulai simulasi dapat dilakukan dengan callback yang meneruskan ID pertandingan sebagai parameter.

  • Kemudian, jembatan meneruskannya melalui AIDL, lalu ke aplikasi Unity, tempat C# mendapatkan perintah dan mulai mengonfigurasi scene.

    public class NativeInterfaceListener : AndroidJavaProxy {
    [UsedImplicitly]
    public void startMatch(string matchID) {
       UnityThreadHelper.InvokeOnMain(() => _internalAPI.StartMatch(matchID));
    }
    }
  • Kita harus menunggu Unity aktif dan siap menekan perintah yang kita teruskan ke Unity.

  • Aktivitas Unity yang menggabungkan aplikasi akan aktif sebelum Unity itu sendiri aktif.

  • Jika perintah dikirim terlalu awal, kami akan menyimpannya dalam antrean callback.

    fun onStartMatchClickedInternal() {
       if (isServiceBound) {
           AIDLBridgeService.startMatch("match_id")
       } else {
           delayedCallbacksQueue.add(action)
       }
    }
  • Setelah Unity selesai melakukan boot loading dan inisialisasi, dan kita berada dalam status yang dianggap aman, kita berkomunikasi dari C# bahwa Unity sudah siap.

    // After bootload and initialization
    
    using AndroidJavaClass nativeBridge = new("com.company.UnityActivity");
    using AndroidJavaObject instance = nativeBridge.GetStatic<AndroidJavaObject>("instance");
    instance.Call("onUnityReady");
  • Setelah itu, kita akan mengulang antrean dan meneruskan semua perintah yang disimpan.

    @Suppress("unused") //  Unity implicit call
    fun onUnityReady() {
       bridge.activityBridgeServiceInternal?.onUnityReady()
    }
    
    delayedCallbacksQueue.forEach { it(activityBridgeService) }
  • Diagram ini mengilustrasikan dengan lebih baik cara kerja komunikasi antar-proses kami.

  • Seperti yang Anda lihat, dengan menggunakan jembatan di antara aktivitas, kita dapat menangani callback yang telah kita sebutkan.

  • Kami menyiapkan StateFlow yang diupdate dari Unity untuk memantau nilai seperti status autentikasi atau stempel waktu status saat ini dari pertandingan.

  • Selain itu, kami juga mengelola enkripsi kredensial, dan meneruskan error apa pun dari Unity sehingga NBA dapat melakukan debug dengan lebih mudah dan memutuskan cara terbaik untuk menanganinya di tingkat aplikasi.

  • Itu saja dari saya.

  • Sekarang, saya akan mempersilakan Josh untuk membahas lebih lanjut cara mereka mengintegrasikan paket Court View Unity ke dalam aplikasi NBA.

  • Kita menggunakan dependensi Gradle ini untuk mengintegrasikan Unity sebagai library dengan aplikasi Jetpack Compose kita: XR Runtime untuk mengakses sesi 3D, Jetpack SceneCore untuk menghosting aktivitas Unity dan mengalihkan lingkungan spasial, serta Jetpack Compose untuk XR guna mengakses panel spasial dalam kode Compose kita.

    dependencies {
      implementation("androidx.xr.runtime:runtime:1.0.0")
      implementation("androidx.xr.scenecore:scenecore:1.0.0")
      implementation("androidx.xr.compose:compose:1.0.0-beta02")
    }
  • Selanjutnya, mari kita lihat cara merender Tampilan Lapangan di Jetpack Compose.

    // Step 1: Request Full Space
    LocalSession.current?.scene?.requestFullSpace()
    
    // Step 2: A shared Subspace for NBA app content and embedded Unity Activity.
    override fun onCreate(savedInstanceState: Bundle?){
        setContent {
            Subspace {
               SpatialPanel { AppContent() }
    
               // Court View
               val intent = courtViewFeature.intent
               val controller = rememberSpatialActivityPanelController(intent)
               SpatialActivityPanel(controller)
            }
        }
    }
  • Sebelum meluncurkan Tampilan Lapangan, kami akan membawa Anda ke ruang penuh.

  • Tindakan ini diperlukan untuk menampilkan panel spasial yang menampung Unity.

  • Selanjutnya, kami mengirimkan informasi game melalui AIDL untuk meluncurkan Court View untuk game tertentu.

  • Unity kemudian akan dimulai.

  • Arsip Android kami mengirimkannya sebagai intent ke aktivitas Unity, dan kami menggabungkannya dalam SpatialActivityPanelController.

  • Dari sana, kita cukup meneruskan pengontrol tersebut ke SpatialActivityPanel.

  • Panel ini adalah tempat Court View, dan dibagikan di subruang kita dengan panel spasial utama kita.

  • Dengan begitu, kita dapat mengontrol keduanya sekaligus tanpa masalah.

  • Selesai.

  • Saya akan mengirimkannya kembali ke Luke.

Kesimpulan dan referensi

  • Terima kasih banyak.

  • Seperti yang Anda lihat, NBA dapat mempercepat Court View dengan memisahkan pengembangan Unity dan Jetpack Compose.

  • Untuk mempelajari lebih lanjut cara memulai penggunaan penampung spasial dan menggunakannya dalam aplikasi Android XR Anda sendiri, lihat dokumentasi developer di developer.android.com/develop/xr.

  • Terima kasih banyak telah bergabung bersama kami hari ini, dan kami tidak sabar untuk melihat apa yang Anda bangun.

Kode

Cuplikan kode

Cuplikan Skrip Kotlin pada menit 09.30 di "Communication between Kotlin and Godot"
androidResources {
     ignoreAssetsPattern = "!.svn:!.git:!.gitignore:!.ds_store:!*.scc:<dir>_*:!CVS:!*~:!android:!export_presets.cfg"
 }
Cuplikan Skrip Kotlin pada menit 10:27 di "Komunikasi antara Kotlin dan Godot"
dependencies {
    // Godot 4.8 Android Library + Godot OpenXR Vendors plugin + Khronos OpenXR Loader
    implementation("org.godotengine:godot:4.8.0.dev-SNAPSHOT")
    implementation("org.godotengine:godot-openxr-vendors-androidxr:5.1.0-stable")
    implementation("org.khronos.openxr:openxr_loader_for_android:1.1.63")

    // Jetpack XR SDK (Compose for XR, SceneCore, Runtime)
    implementation("androidx.xr.compose:compose:1.0.0-beta02")
    implementation("androidx.xr.scenecore:scenecore:1.0.0")
    implementation("androidx.xr.runtime:runtime:1.0.0")
Cuplikan Kotlin pada menit 10:48 di "Merender Godot di Jetpack Compose"
@Composable
fun GodotView(godotHost: GodotHost, modifier: Modifier = Modifier) {
    // 1. Initialize Godot's native render surface
    val renderView = remember(godotHost) {
        godotHost.godot!!.onInitRenderView(godotHost)
    }
    // 2. Host the SurfaceView inside Jetpack Compose
    AndroidView(
        modifier = modifier,
        factory = { renderView }
    )
}
Cuplikan Kotlin pada 10:55 di "Merender Godot di Jetpack Compose"
Subspace {
        SpatialPanel(
            modifier = SubspaceModifier
                .width(960.dp)
                .height(540.dp)
                .offset(z = (-200).dp)
                .movable()
,
    ) {
        GodotView(
            godotHost = godotHost,
            modifier = Modifier.fillMaxSize(),
        )
    }
}
Cuplikan Kotlin pada menit 11:05 di "Rendering Godot di Jetpack Compose"
@Composable
fun GodotView(
   godotHost: GodotHost,
   modifier: Modifier = Modifier,
   lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
) {

   DisposableEffect(lifecycleOwner, godotHost) {
       val observer = LifecycleEventObserver { _, event ->
           val engine = godotHost.godot ?: return@LifecycleEventObserver
           when (event) {
               Lifecycle.Event.ON_START -> engine.onStart(godotHost)
….
               Lifecycle.Event.ON_DESTROY -> engine.onDestroy(godotHost)
               else -> {}
           }
       }

       lifecycleOwner.lifecycle.addObserver(observer)

       onDispose {
           lifecycleOwner.lifecycle.removeObserver(observer)
       }
   }

}
Cuplikan Kotlin pada menit 11:29 di "Rendering Godot di Jetpack Compose"
class MainActivity : ComponentActivity(), GodotHost {
    override fun getHostPlugins(engine: Godot): Set<GodotPlugin> =
        setOf(SoccerGodotPlugin(engine))
}
Cuplikan Kotlin pada menit 11:52 di "Merender Godot di Jetpack Compose"
class SoccerGodotPlugin(godot: Godot) : GodotPlugin(godot) {
    override fun getPluginName() = "SoccerPlugin"
    // 1. Signals sent from Android Compose UI -> Godot
    override fun getPluginSignals() = setOf(
        SignalInfo("camera_mode_changed", String::class.java),
        SignalInfo("match_command", String::class.java),
    )

}
Cuplikan teks pada 12:01 di "Merender Godot di Jetpack Compose"
var soccer_plugin: Object = null

func _ready() -> void:
    if Engine.has_singleton("SoccerPlugin"):
        soccer_plugin = Engine.get_singleton("SoccerPlugin")

        # Listen for signals from Jetpack Compose UI
        soccer_plugin.connect("camera_mode_changed", _on_camera_mode_changed)
        soccer_plugin.connect("match_command", _on_match_command)
Cuplikan Kotlin pada menit 12:10 di "Rendering Godot di Jetpack Compose"
class SoccerGodotPlugin(godot: Godot) : GodotPlugin(godot) {

    fun sendCommandToGodot(command: String) {
        emitSignal("match_command", command)
    }
}
Cuplikan teks pada menit 12:20 di "Rendering Godot di Jetpack Compose"
func _on_goal_scored(minute: int, home: int, away: int) -> void:

    if soccer_plugin:
        soccer_plugin.onMatchEvent("GOAL", minute, home, away)
Cuplikan Kotlin pada menit 12:30 di "Rendering Godot di Jetpack Compose"
@UsedByGodot
fun onMatchEvent(type: String, minute: Int, home: Int, away: Int, title: String) {

        _uiState.update { state ->
            state.copy(homeScore = home, awayScore = away, matchSeconds = minute)
        }
    }
Cuplikan Kotlin pada menit 14:35 di "Merender Godot di Jetpack Compose"
LaunchedEffect(uiState.phase) {
    while (uiState.phase == MatchPhase.PLAYING) {
        delay(1000L) // Update scoreboard timer at 1 Hz, not 90 Hz
        SoccerGodotPlugin.tickLocalClock()
    }
}
...
@UsedByGodot
fun onMatchEvent(eventTypeStr: String, matchMinute: Int, ...) {
    // Recompose once when threshold is crossed to display the popup banner
    _uiState.update { it.copy(activeBannerEvent = event, ...) }
}
Cuplikan Kotlin pada 14:57 di "Rendering Godot di Jetpack Compose"
private val mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
mainScope.launch {
   _uiState.update { current ->
        current.copy(
           homeScore = homeScore,
           awayScore = awayScore,
           matchSeconds = matchMinute.coerceIn(0, 90),
           phase = updatedPhase,
           events = newEventsList,
           activeBannerEvent = event
       )
   }
Cuplikan XML pada menit 15:22 di "Merender Godot di Jetpack Compose"
<uses-native-library
           android:name="libopenxr.google.so"
           android:required="false" />

           <!-- 1. Launch Activity in Full-Space Managed XR mode -->
           <property
               android:name="android.window.PROPERTY_XR_ACTIVITY_START_MODE"
               android:value="XR_ACTIVITY_START_MODE_FULL_SPACE_MANAGED" />

           <!-- 2. OpenXR Spatial Container bounds mode hints -->
           <property
               android:name="org.khronos.openxr.PROPERTY_ACTIVITY_HINT_XR_SPATIAL_CONTAINER_USED_BOUNDS_MODES"
               android:value="" />
Cuplikan Skrip Kotlin pada menit 20:41 di "Mengintegrasikan Unity sebagai library"
// The Unity build
include(":unityLibrary")
project(":unityLibrary").projectDir = File("..\\Libraries\\unityBuild\\unityLibrary")

// Custom communication IPC module
include(":nativeCommunications")
project(":nativeCommunications").projectDir = File("..\\Libraries\\nativeCommunications")

// If using XR features
include(":unityLibrary:xrmanifest.androidlib")

// Used for merging libraries, exporting and publishing.
include(":packageExporter")

dependencyResolutionManagement {
   repositories {
flatDir { // Required to load unity correctly
   dirs("${project(":unityLibrary").projectDir}/libs")
}
}
Cuplikan Kotlin pada menit 22:11 di "Mengintegrasikan Unity sebagai library"
// Main Activity

val intent = Intent(context, AIDLBridgeService::class.java)
context.startService(intent)
context.bindService(intent, connection, 0)

fun onStartMatchClicked() {
   bridge.runWithService { service ->
       service.startMatch("match_id")
   }
}

// AIDL Bridge Service

private val remoteBinder = object : IBridgeService.Stub() {
override fun startMatch(matchID: String?) {
   addToCallbackQueue { nativeInterface?.startMatch(matchID) }
}
}
Cuplikan C# pada menit 22:36 di "Mengintegrasikan Unity sebagai library"
public class NativeInterfaceListener : AndroidJavaProxy {
[UsedImplicitly]
public void startMatch(string matchID) {
   UnityThreadHelper.InvokeOnMain(() => _internalAPI.StartMatch(matchID));
}
}
Cuplikan Kotlin pada menit 22:54 di "Mengintegrasikan Unity sebagai library"
fun onStartMatchClickedInternal() {
   if (isServiceBound) {
       AIDLBridgeService.startMatch("match_id")
   } else {
       delayedCallbacksQueue.add(action)
   }
}
Cuplikan C# pada menit 22:57 di "Mengintegrasikan Unity sebagai library"
// After bootload and initialization

using AndroidJavaClass nativeBridge = new("com.company.UnityActivity");
using AndroidJavaObject instance = nativeBridge.GetStatic<AndroidJavaObject>("instance");
instance.Call("onUnityReady");
Cuplikan Kotlin pada menit 23:06 di "Mengintegrasikan Unity sebagai library"
@Suppress("unused") //  Unity implicit call
fun onUnityReady() {
   bridge.activityBridgeServiceInternal?.onUnityReady()
}

delayedCallbacksQueue.forEach { it(activityBridgeService) }
Cuplikan Skrip Kotlin pada menit 23:50 di "Mengintegrasikan Unity sebagai library"
dependencies {
  implementation("androidx.xr.runtime:runtime:1.0.0")
  implementation("androidx.xr.scenecore:scenecore:1.0.0")
  implementation("androidx.xr.compose:compose:1.0.0-beta02")
}
Cuplikan Kotlin pada menit 24:10 di "Mengintegrasikan Unity sebagai library"
// Step 1: Request Full Space
LocalSession.current?.scene?.requestFullSpace()

// Step 2: A shared Subspace for NBA app content and embedded Unity Activity.
override fun onCreate(savedInstanceState: Bundle?){
    setContent {
        Subspace {
           SpatialPanel { AppContent() }

           // Court View
           val intent = courtViewFeature.intent
           val controller = rememberSpatialActivityPanelController(intent)
           SpatialActivityPanel(controller)
        }
    }
}