Android XR

Ulepszanie immersyjnych doświadczeń dzięki silnikom 3D w Jetpack Compose for XR

Głośniki

Informacje

Informacje o wystąpieniu

Dowiedz się, jak zintegrować silniki gier 3D (Godot i Unity) z Jetpack Compose w jednej bazie kodu na urządzenia mobilne i Androida XR. Omówimy osadzanie scen 3D w kontenerach przestrzennych i konfigurowanie komunikacji między Kotlinem a silnikiem. Przyjrzymy się też, jak NBA stworzyła transmisje na żywo z meczów koszykówki na Androida XR.

Podsumowanie

  • Rozszerzenie OpenXR Spatial Containers umożliwia silnikom 3D współdzielenie przestrzeni z natywnym interfejsem Androida. Zamiast przejmować kontrolę nad skyboxem, silniki działające w czasie rzeczywistym działają w zarządzanej przestrzeni 3D obok paneli przestrzennych Jetpack XR.
  • Deweloperzy mogą umieszczać silniki działające w czasie rzeczywistym bezpośrednio w aplikacjach Jetpack Compose jako biblioteki AAR. To podejście pozwala natywnej aplikacji na Androida obsługiwać logikę biznesową i interfejs, podczas gdy silnik renderuje komponenty 3D.
  • Osadzenie Godota wymaga zależności AAR i reguły kompilacji AAPT, aby zachować ukryte zasoby. Podklasa GodotPlugin obsługuje następnie komunikację dwukierunkową z Kotlinem.
  • Pętle interfejsu Compose muszą być oddzielone od pętli ciągłego cyklu silnika. Przekazywanie aktualizacji silnika bez sprawdzania powoduje ciągłą rekompozycję i opóźnienia interfejsu.
  • Aplikacja NBA integruje Unity jako wbudowaną bibliotekę w osobnym procesie, używając AIDL do komunikacji między procesami, co chroni aplikację hosta przed awariami.

Zasoby

Gemini Notebook

Tematy

  • android-xr
  • jetpack-compose
  • kontenery-przestrzenne
  • openxr
  • godot
  • jedność

Transkrypcja

Pobierz transkrypcję

Wprowadzenie do Androida XR

  • Cześć!

  • Nazywam się Luke Hopkins.

  • Jestem inżynierem ds. relacji z deweloperami w zespole Androida XR.

  • W dzisiejszej prezentacji omówimy, jak można umieszczać silniki gier, takie jak Godot i Unity, w projekcie aplikacji na Androida.

  • Później dołączą do mnie Josh i Rodrigo z NBA i Beyond Sports, którzy opowiedzą o tym, jak wdrażają Unity jako bibliotekę w aplikacji NBA.

  • Podczas tej sesji omówię, jak zintegrować Godot z aplikacją Jetpack Compose, a następnie Josh i Rodrigo przedstawią swoją aplikację NBA.

  • Android XR to rozszerzenie ekosystemu Androida, które obejmuje szeroką gamę formatów, aby nadać znane i lubiane funkcje nowy wymiar.

  • Zadbaliśmy o to, aby przenoszenie istniejących aplikacji XR i aplikacji na Androida do tej nowej przestrzeni było intuicyjne. Wykorzystujemy też istniejące technologie Androida, których już używasz.

  • Android XR to ujednolicona platforma zaprojektowana z myślą o szerokiej gamie urządzeń.

  • Urządzenia z Androidem XR obsługują różne rodzaje treści.

  • Mamy ulepszone funkcje audio i okulary z wyświetlaczem, a także immersyjne gogle XR i przewodowe okulary XR.

  • Dzisiaj omówimy tworzenie immersyjnych doświadczeń na przewodowe okulary XR i gogle XR.

  • Wciągające doświadczenia można tworzyć za pomocą pakietu Jetpack XR SDK lub OpenXR z silnikiem działającym w czasie rzeczywistym.

  • Obsługujemy Unity, Godot i Unreal, dzięki czemu możesz tworzyć immersyjne treści na Androida niezależnie od tego, jakiego narzędzia używasz.

  • W tym wystąpieniu wyjaśnię, jak można to osiągnąć, wykorzystując interfejs API SceneCore Jetpacka wraz z Godotem i nowym rozszerzeniem OpenXR.

  • Tutaj widzisz silnik Godot działający obok paneli przestrzennych Jetpacka.

  • Zwróć uwagę, że Godot jest renderowany jako treść przestrzenna, co zapewnia objętość 3D obok interfejsu Compose UI 2D w kontenerze przestrzennym.

  • Dzięki temu programiści aplikacji na Androida mogą uruchamiać symulacje fizyki i niestandardowe shadery przy użyciu wybranego silnika działającego w czasie rzeczywistym, a w pozostałej części aplikacji stosować natywne metody programowania na Androida.

  • Dzięki Androidowi XR i XREAL AURA Ty też możesz tworzyć takie rozwiązania.

  • W tej prezentacji pokażemy, jak stworzyć takie środowisko za pomocą Godot i Android Studio. Następnie nasi partnerzy z NBA i Beyond Sports opowiedzą, jak stworzyli je przy użyciu Unity jako biblioteki.

Kontenery przestrzenne

  • Wcześniej tworzenie środowiska XR za pomocą OpenXR oznaczało tworzenie środowiska, które renderuje każdy piksel.

  • Oznacza to, że jeśli deweloper musi wyświetlić drugą aktywność, nie może mieć obu jednocześnie.

  • Aby rozwiązać ten problem, Khronos, grupa robocza OpenXR, we współpracy z kilkoma liderami branży, wydała rozszerzenie OpenXR Spatial Containers.

  • Kontenery przestrzenne można porównać do OpenXR w pudełku.

  • Zamiast przejmować cały skybox, silnik otrzymuje zarządzany przez system obszar 3D, który może współistnieć z panelami przestrzennymi XR Jetpacka, innymi kontenerami przestrzennymi i standardowymi aktywnościami na Androidzie.

  • Załóżmy, że chcesz wykorzystać Androida XR, wyświetlając 2 panele przestrzenne jednocześnie podczas grania w grę immersyjną.

  • Wszystko renderujesz w silniku, co w tych przypadkach umożliwia jednoczesne działanie edytora Godot i kontenera przestrzennego.

  • Od poziomu 4 interfejsu Spatial API Google wdraża kontenery przestrzenne w Androidzie XR.

  • Kontenery przestrzenne obsługują 2 ważne tryby wyświetlania.

  • Pierwszy to tryb ograniczony.

  • Zapewnia to zdefiniowaną przestrzeń 3D z kilkoma wbudowanymi funkcjami unikania.

  • Użytkownik może chwycić kontener przestrzenny, obrócić go, ustawić, zminimalizować, a także korzystać z projekcji głębi stereo, co daje efekt magicznego okna lub wyskakującego okienka 3D, a także umożliwia przycinanie krawędzi kontenera.

  • Drugi to tryb nieograniczony.

  • Jeśli użytkownik zdecyduje, że chce pełnej immersji, aplikacja może poprosić o płynne przejście z trybu ograniczonego do trybu pełnej przestrzeni, a następnie możesz też wrócić do trybu ograniczonego.

  • Dzięki dodaniu kontenerów przestrzennych Android XR otwiera drzwi dla nowych i rozbudowanych pomysłów.

  • Załóżmy, że chcesz przenieść interfejs Androida do gry lub utworzyć niestandardowy cieniujący do wyświetlania treści 3D w aplikacji.

  • Przed wprowadzeniem kontenerów przestrzennych nie było możliwe jednoczesne uruchomienie Compose i silnika gry.

  • Jeśli chodzi o tworzenie aplikacji w silnikach działających w czasie rzeczywistym, również funkcjonowaliśmy w 2 różnych paradygmatach.

  • W przypadku programistów aplikacji natywnych na Androida środowisko wykonawcze aplikacji jest oparte na Jetpack Compose.

  • Został zaprojektowany z myślą o szybkim, reaktywnym interfejsie, wydajności baterii, dynamicznych układach, ułatwieniach dostępu i płynnym wprowadzaniu tekstu.

  • Gdy nic się nie dzieje, jest w trybie bezczynności i działa w przewidywalny sposób.

  • W przypadku deweloperów silników gier domena to renderowanie w czasie rzeczywistym, gdzie narzędzia takie jak Godot i Unity sprawdzają się najlepiej.

  • Nie czekają tylko na interakcję użytkownika.

  • Działają w pętli ciągłego renderowania i odświeżania.

  • Może to być 60, 90, 100 klatek na sekundę.

  • Wykonują obliczenia fizyczne, złożone cieniowanie, animacje szkieletowe i odtwarzają dźwięk przestrzenny, a wszystko to z niesamowitą wydajnością.

Sposoby integracji silników 3D

  • Co się stanie, jeśli chcesz połączyć te 2 rozwiązania?

  • Wcześniej deweloperzy musieli wdrożyć jedno z tych 2 podejść.

  • Opcja A polega na przepisaniu całej aplikacji w prawdziwym silniku, takim jak Godot czy Unity.

  • Wyrzucenie istniejącej bazy kodu oznacza utratę dużej części natywnego interfejsu Compose, ponowne wdrażanie podstawowego tekstu i nawigacji oraz pisanie kodu wokół interfejsów API Androida, takich jak zamiana tekstu na mowę.

  • Opcja B polega na utworzeniu od podstaw potoku renderowania grafiki 3D niskiego poziomu w aplikacji natywnej przy użyciu nieprzetworzonych interfejsów API Vulkan i OpenXR.

  • Zamiast dostarczać funkcje produktu, tworzysz własny silnik.

  • Dlatego dzisiaj pokażę, jak najlepiej wykorzystać Godot i Unity jako biblioteki.

  • Dzięki dodaniu kontenerów przestrzennych możesz teraz korzystać z obu tych rozwiązań.

  • Możemy to osiągnąć, umieszczając silnik działający w czasie rzeczywistym bezpośrednio w bibliotece w aplikacji Jetpack.

  • Zachowujesz strukturę produkcyjną aplikacji na Androida, logikę biznesową i płynne renderowanie treści przestrzennych 3D w aplikacji.

  • Oznacza to również, że możesz mieć jeden plik APK dla wszystkich formatów urządzeń.

  • Jeśli nie chcesz wyświetlać treści 3D, możesz nie wczytywać biblioteki do aktywności lub fragmentu i zachować przejrzystość bazy kodu, co pozwoli Ci obsługiwać wiele urządzeń.

  • Filozofia jest prosta: oddzielić aplikację i uczynić z silnika czasu rzeczywistego bibliotekę.

  • Zamiast wybierać między aplikacją na Androida a silnikiem, traktujesz silnik jako wbudowaną bibliotekę oprogramowania, nazywaną też zależnością AAR.

  • Możesz pobrać go bezpośrednio do kompilacji Gradle, tak jak każdą inną bibliotekę.

  • Natywny kod Androida pozostaje głównym elementem aplikacji, która hostuje komponenty 3D.

  • Jetpack Compose XR zajmuje się tym, co robi najlepiej: renderowaniem natywnego, dostępnego i płynnego interfejsu oraz zarządzaniem stanem aplikacji.

  • Gdy potrzebny jest element 3D, aplikacja udostępnia osadzony widok renderowania, który jest renderowany przez sam silnik gry.

  • Dzisiaj szczegółowo omówimy, jak to działa w przypadku silnika Godot.

  • Zacznę od mechanizmu osadzania i nawiązywania dwukierunkowej komunikacji między Kotlinem a GDScript.

  • Następnie przyjrzymy się nowemu rozszerzeniu OpenXR, Spatial Container, które naprawdę otworzy drogę do nowej generacji wielozadaniowości i układu przestrzennego.

  • Przyjrzyjmy się bliżej wersji demonstracyjnej, którą pokazałem wcześniej.

  • Do stworzenia tej gry użyłem Godot i Android Studio.

  • Ten klip pokazuje, jak Jetpack Compose for XR renderuje natywne panele przestrzenne interfejsu 2D na potrzeby tablicy wyników, powiadomień i najciekawszych momentów.

  • Oprócz tego Godot renderuje symulację meczu piłki nożnej.

Komunikacja między Kotlinem a Godotem

  • Tutaj możesz zobaczyć zestawienie informacji o tym, jak komunikujemy się z Kotlinem w GDScript.

  • W tym celu Godot udostępnia architekturę wtyczek.

  • Piszesz lekką klasę Kotlin, która rozszerza GodotPlugin.

  • Ta klasa służy jako interfejs między JVM a środowiskiem wykonawczym C++ Godot.

  • Dzięki temu interakcje między aplikacją a Godotem są bardzo proste.

  • Architektura Godot obsługuje komunikację dwukierunkową między Godot i Kotlinem.

  • Gdy w Godot wystąpi zdarzenie w grze, np. gol lub faul, Godot może bezpośrednio wywołać funkcje Kotlin i aktualizować interfejs Compose w czasie rzeczywistym.

  • Gdy użytkownik wejdzie w interakcję z odtwarzaczem multimedialnym, np. wstrzyma odtwarzanie lub wybierze inne najciekawsze momenty, Kotlin wyśle sygnał z powrotem do Godota i zostaną wykonane odpowiednie działania silnika gry.

  • Po przekroczeniu wtyczki Godot wydajność i zarządzanie po prostu spadają wzdłuż linii domeny.

  • Stosuj sprawdzone metody dotyczące aplikacji na Androida i projektów Godot.

  • Teraz przyjrzymy się architekturze.

  • Zobaczmy, jak połączyć to z Godotem w aplikacji na Androida.

  • Środowisko wykonawcze silnika Godot jest rozpowszechniane przez Maven jako AAR.

  • Integracja z kompilacją odbywa się zgodnie ze standardowymi praktykami dotyczącymi Androida.

  • Jest jeden ważny krok, o którym należy pamiętać od razu, a który nazywam pułapką komponentów.

    androidResources {
         ignoreAssetsPattern = "!.svn:!.git:!.gitignore:!.ds_store:!*.scc:<dir>_*:!CVS:!*~:!android:!export_presets.cfg"
     }
  • W projekcie Godot sceny i pliki konfiguracji projektu są przechowywane razem.

  • W przypadku osadzania silnika Godot projekt Godot jest dodawany bezpośrednio do folderu komponentów.

  • Oznacza to, że wszystkie zaimportowane siatki 3D, skompresowane tekstury i skompilowane tekstury są przechowywane jako artefakt środowiska wykonawczego w ukrytym pliku o nazwie .godot.

  • Domyślnie Android usuwa ten folder podczas kompilacji, więc ignoruje każdy katalog z kropką.

  • Jeśli więc zsynchronizujesz i utworzysz aplikację w normalny sposób, pakiet APK zostanie utworzony, ale ulegnie awarii i nie zostanie wczytany, ponieważ te elementy nie zostały w nim skompilowane.

  • Aby to naprawić, możesz po prostu zastąpić opcje AAPT w pliku build.gradle i zezwolić na kropki na początku w Godot.

  • Gdy to zrobisz, możesz raz uruchomić scenę Godot w edytorze Godot, a następnie utworzyć wszystko inne w Android Studio.

  • Aby obsługiwać kontenery przestrzenne i najnowszą wersję OpenXR, zalecam używanie silnika Godot w wersji 4.8 lub nowszej, rozszerzeń dostawcy Godot w wersji 5.2 i narzędzia Khronos OpenXR loader w wersji 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")

Renderowanie Godota w Jetpack Compose

  • Jak Godot i Compose renderują zasoby po ich spakowaniu?

  • Biblioteka Godot udostępnia metodę fabryczną o nazwie 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 }
        )
    }
  • Zwraca to obiekt SurfaceView, który Godot po prostu renderuje.

  • Możesz więc umieścić go w funkcji typu „composable”, a Godot może teraz zmieniać rozmiar, dodawać dopełnienie i umieszczać go w dowolnym miejscu w panelu encji.

    Subspace {
            SpatialPanel(
                modifier = SubspaceModifier
                    .width(960.dp)
                    .height(540.dp)
                    .offset(z = (-200).dp)
                    .movable()
    ,
        ) {
            GodotView(
                godotHost = godotHost,
                modifier = Modifier.fillMaxSize(),
            )
        }
    }
  • Sam silnik nie może zarządzać cyklem życia.

    @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)
           }
       }
    
    }
  • Dlatego aktywność musi implementować interfejs GodotHost.

  • Dzięki temu wszystkie cykle życia, które występują w ramach aktywności, np. wstrzymanie czy wznowienie, będą wysyłane do silnika Godot, który będzie teraz uwzględniać cykl życia.

  • Następnie musimy dodać naszą aplikację natywną i silnik, aby zapewnić bezproblemową komunikację.

  • W tym celu możemy użyć interfejsu GodotHost.

    class MainActivity : ComponentActivity(), GodotHost {
        override fun getHostPlugins(engine: Godot): Set<GodotPlugin> =
            setOf(SoccerGodotPlugin(engine))
    }
  • Możemy zdefiniować wtyczkę Godot i upewnić się, że po uruchomieniu Godota wtyczka jest zarejestrowana po stronie Compose.

  • Wtyczka jest już zarejestrowana, więc skonfigurujmy Androida do komunikacji przez wysyłanie sygnału do Godota.

  • W kodzie Kotlin utworzymy instancję GodotPlugin.

  • Zarejestrujemy wtyczkę w bibliotece Godot za pomocą aktywności.

  • Następnie w Kotlin tworzymy GodotPlugin, rejestrujemy jego nazwę i przekazujemy Godotowi wszystkie sygnały, których chcemy używać w tym pakiecie.

    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),
        )
    
    }
  • Następnie mamy skrypt Godot, w którym możesz uzyskać instancję tej wtyczki.

    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)
  • Możemy zobaczyć, co jest zarejestrowane, a następnie połączyć funkcje z zarejestrowanymi sygnałami.

  • Gdy wywołasz coś takiego jak emitSignal, możesz przekazać to z kodu Kotlin i trafi to bezpośrednio do Godota.

    class SoccerGodotPlugin(godot: Godot) : GodotPlugin(godot) {
    
        fun sendCommandToGodot(command: String) {
            emitSignal("match_command", command)
        }
    }
  • Aby Godot mógł komunikować się z Kotlinem, np. gdy padnie gol, możemy wywołać funkcję Kotlina bezpośrednio ze skryptu Godota, używając instancji wtyczki.

    func _on_goal_scored(minute: int, home: int, away: int) -> void:
    
        if soccer_plugin:
            soccer_plugin.onMatchEvent("GOAL", minute, home, away)
  • Najpierw dodaj do funkcji Kotlin adnotację 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)
            }
        }
  • Silnik obsługuje przekształcanie JNI w tle.

  • Następnie możemy wywołać funkcję bezpośrednio z Godot Script.

  • Podczas korzystania z tego operatora warto pamiętać o jednej wskazówce: UsedByGodot wymaga dokładnego dopasowania ciągu znaków.

  • Jeśli więc skrypt Godot wywołuje onMatchEvent, nazwa w Kotlin musi być dokładnie taka sama, ponieważ nie ma automatycznej konwersji na camelCase ani snake_case.

  • Musisz więc upewnić się, że obie metody są dokładnie takie same.

  • Po połączeniu wtyczki najważniejszą częścią tej prezentacji jest to, co nazywam modelem mentalnym.

  • W tym miejscu silniki gier i Jetpack Compose działają na 2 zasadniczo przeciwstawnych architekturach.

  • Silnik gry działa w trybie ciągłego cyklu klatek.

  • Niezależnie od tego, czy coś zmienisz, czy nie, proces cyklu silnika gry odbywa się 60, 90, 100 razy na sekundę, zapewniając bogate i płynne renderowanie w czasie rzeczywistym.

  • Silniki takie jak Godot efektywnie wykorzystują cykle GPU i CPU.

  • Z kolei Jetpack Compose obsługuje elementy reaktywnie i jest oparty na stanie.

  • A gdy aplikacja się nie zmienia, Compose dosłownie kontynuuje działanie, zużywając 0% procesora.

  • Jeśli chcesz połączyć te 2 środowiska wykonawcze bez jasnej strategii, te filozofie mogą się ze sobą kolidować.

  • Zobaczmy, jak możemy rozwiązać ten problem za pomocą czegoś, co nazywam „wyciekiem cyklu”.

  • Tutaj mamy element 3D, na przykład zegar tablicy wyników.

  • W skrypcie silnika znajduje się zmienna licznika czasu, która stale się zmienia.

  • Jeśli bezmyślnie przekażesz tę zmienną do Compose, powiedzmy 90 razy na sekundę, wejdziesz w pętlę rekompozycji 90 razy na sekundę.

  • Oznacza to, że Compose będzie ponownie oceniać elementy kompozycyjne, mierzyć układy i rysować drzewa interfejsu 90 razy na sekundę.

  • Będziesz widzieć gubienie klatek, opóźnienia interfejsu i szybko doświadczysz dławienia termicznego.

  • Złota zasada brzmi: nigdy nie pozwól, aby pętla silnika ciągłego dyktowała pętlę interfejsu reaktywnego.

  • W tym przykładzie zamiast ciągłego aktualizowania licznika czasu na tablicy wyników przez Godota, Android pobiera te dane raz na sekundę, gdy są potrzebne.

    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, ...) }
    }
  • Na koniec pamiętaj o granicy wątku.

  • Pętla silnika gry Godot jest stale wykonywana we własnym wątku renderowania.

  • Interfejsy użytkownika Androida muszą być wykonywane w głównym wątku Androida.

  • Dlatego przed zaktualizowaniem stanów modelu widoku zawsze upewnij się, że wtyczka Kotlin wysyła zdarzenia za pomocą Dispatchers.Main lub elementów podstawowych stanu bezpiecznego.

    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
           )
       }
  • Dzięki zachowaniu pracy o wysokiej częstotliwości w silniku i odrębnej semantyki w Compose możesz uzyskać płynne widoki 3D bez wpływu na czas pracy na baterii, ułatwienia dostępu czy wydajność aplikacji natywnej.

  • Teraz zobaczmy, jak dodać kontenery przestrzenne do projektu Godot.

  • Najpierw skonfigurujemy aplikację tak, aby działała w trybie pełnej przestrzeni, włączymy kontenery przestrzenne ze wskazówkami dotyczącymi granic, a następnie ustawimy bibliotekę programu ładującego XR w pliku manifestu Androida.

    <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 ma własne ustawienia projektu XR, w których mogę włączyć OpenXR, shadery XR i kontenery przestrzenne bezpośrednio w edytorze.

  • To w zasadzie wszystko, co musisz zrobić, aby włączyć kontenery przestrzenne w Godot.

  • Teraz, gdy Godot działa z Androidem XR, kontenerami przestrzennymi i wszystkimi tymi funkcjami, zalecamy zapoznanie się z najlepszymi praktykami dotyczącymi korzystania z kontenerów przestrzennych, paneli przestrzennych i JXR na stronie dla deweloperów.

  • Właśnie omówiliśmy przykład użycia kontenerów przestrzennych w silniku Godot i Androidzie XR. Jestem pewien, że wielu z Was chciałoby usłyszeć opinię dewelopera, który aktywnie pracuje nad kontenerami przestrzennymi na Androida XR.

  • Teraz chciałbym zaprosić Josha i Rodrigo z NBA, którzy są jednymi z pierwszych partnerów korzystających z kontenerów przestrzennych w swojej aplikacji.

Widok boiska NBA – omówienie

  • Dziękuję, Luke.

  • Cześć wszystkim.

  • Nazywam się Josh i jestem programistą aplikacji na Androida w NBA.

  • Jestem tu dzisiaj z moim współpracownikiem, Rodrigo Sanchezem, programistą oprogramowania Unity w firmie Beyond Sports.

  • Dzisiaj chcemy pokazać, jak wykorzystaliśmy Unity as a Library do stworzenia widoku boiska w aplikacji NBA.

  • W NBA zawsze staramy się zapewniać fanom nowe wrażenia.

  • Dlatego we współpracy z Google i Beyond Sports z przyjemnością przedstawiamy widok boiska.

  • Widok boiska oferuje trójwymiarowe odtworzenie meczów NBA, dzięki czemu subskrybenci League Pass mogą oglądać je z nowych perspektyw, które nie są dostępne w przypadku zwykłych transmisji wideo.

  • Zmień rozmiar i obróć kort, a następnie umieść go w dowolnym miejscu w przestrzeni.

  • Możesz poruszać się po nim i oglądać grę z różnych perspektyw.

  • Przez cały czas możesz normalnie korzystać z aplikacji NBA, od wyświetlania statystyk synchronizacji po oglądanie transmisji na żywo.

  • Jak to wszystko skonfigurować?

  • Widok kortu jest renderowany w Unity i wykorzystywany przez naszą aplikację na Androida, która używa Unity jako biblioteki.

  • Następnie tworzymy jedno archiwum Androida, hostowane w Maven Central, które aplikacja NBA może pobrać jako pojedynczą zależność.

  • Dzięki temu mogliśmy podzielić pracę między Androida i Unity.

  • Dzięki temu każdy z nas może skupić się na tym, co zna najlepiej, i pracować równolegle, aby przyspieszyć ogólny rozwój.

  • Na przykład gdy nasz zespół pracował nad pełną przestrzenią w Compose, zespół Unity mógł skupić się na skalowaniu i układzie boiska 3D.

  • Teraz Rodrigo opowie więcej o projekcie Unity i o tym, jak wykorzystano Unity jako bibliotekę.

Integracja Unity jako biblioteki

  • Dziękuję, Josh.

  • W Beyond Sports od kilku lat współpracujemy z NBA nad tworzeniem wizualizacji 3D na podstawie rzeczywistych danych z meczów.

  • W ramach tego projektu dostosowaliśmy naszą symulację 3D, aby była zgodna z Androidem XR.

  • Nasza symulacja jest oparta na silniku Unity z C#.

  • Eksportujemy symulację do Androida z Unity jako biblioteką i tworzymy obok niej warstwę komunikacyjną za pomocą Kotlina.

  • Zanim połączymy te 2 moduły w jedną zależność Maven, którą NBA może uwzględnić w swojej aplikacji.

  • Na ekranie widać krótki film z tej samej symulacji, którą Josh pokazał wcześniej, ale uruchomionej w edytorze.

  • Aby to zrobić, używamy pakietu rozszerzeń Androida XR dla Unity.

  • Przeprowadzę Cię przez ten proces.

  • Zanim więc zaczniesz tworzyć w Unity, upewnij się, że masz wybrane odpowiednie ustawienia i wtyczki.

  • Pobierz pakiet XR Plug-in Management od Unity i pakiet Android XR Extensions for Unity od Google.

  • Skonfiguruj je w ustawieniach zarządzania wtyczkami XR, aby używać dostawcy rozszerzeń Androida XR OpenXR.

  • W naszym przypadku chcieliśmy użyć kontenerów przestrzennych, więc włączyliśmy tę funkcję i wyłączyliśmy immersyjną rzeczywistość rozszerzoną w ustawieniach zarządzania sesją.

  • Nie użyliśmy żadnego pakietu danych wejściowych XR, ponieważ wszystkie dane wejściowe są obsługiwane natywnie, poza Unity.

  • Następnie w ustawieniach odtwarzacza musisz ustawić projekt tak, aby używał minimalnego poziomu API 24 lub wyższego, a punkt wejścia aplikacji musi być ustawiony na nowszą aktywność w grze.

  • Pamiętaj, że pole Aktywność musi pozostać niezaznaczone.

  • Interesuje nas tylko kompilacja na architekturę ARM64, więc wybieramy tę architekturę docelową, a w sekcji rozdzielczości włączamy opcję uruchamiania w tle, aby można było wykonywać wiele zadań jednocześnie w innych aplikacjach i nadal mieć uruchomioną symulację 3D.

  • Włączamy też aktywność z możliwością zmiany rozmiaru, ponieważ jest ona wymagana do renderowania okien, takich jak prośby o uprawnienia systemowe.

  • Na koniec zalecamy renderowanie aplikacji na Androida XR za pomocą uniwersalnego potoku renderowania i interfejsu Vulkan Graphics API.

  • W zasobie URP należy wyłączyć HDR i przetwarzanie końcowe.

  • Po wykonaniu wszystkich tych czynności możesz utworzyć kompilację.

  • Przejdź do okna kompilacji. Jeśli używasz Unity 6.1 lub nowszej wersji, możesz wybrać profil kompilacji Androida XR, który umożliwia rozdzielenie ustawień, jeśli tworzysz też aplikacje na urządzenia mobilne z Androidem.

  • Przed utworzeniem upewnij się, że eksportowanie projektu jest włączone.

  • Po zakończeniu kompilacji w Unity otrzymasz działający projekt aplikacji na Androida, który można skompilować w Android Studio i uruchomić na urządzeniu jak każdy inny plik APK Unity.

  • W Android Studio musisz przekształcić wyeksportowaną kompilację w bibliotekę, która będzie używana przez inną aplikację.

    // 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")
    }
    }
  • W naszym projekcie mamy 4 różne moduły: główny moduł APK do testowania jako obiekt zastępczy makiety aplikacji NBA, moduł Unity Library zawierający wyeksportowaną kompilację, moduł nativeCommunications zawierający warstwę komunikacji utworzoną między aktywnościami oraz moduł packageExporter, którego używamy do scalania plików AAR w jeden pakiet do dostarczenia do Mavena.

  • Scalanie wielu plików AAR można przeprowadzić za pomocą biblioteki Android Fused Library lub starszych metod społeczności, takich jak Fat AAR.

  • Możesz zachować prostą strukturę i komunikację, utrzymując Unity i główną aktywność w tym samym procesie.

  • Zrezygnowaliśmy z tego i postanowiliśmy oddzielić aktywność Unity w osobny proces, aby zabezpieczyć się na wypadek awarii części Unity. Dzięki temu aplikacja NBA nie zostanie dotknięta awarią i będzie mogła bezpiecznie zamknąć Unity bez awarii.

  • Ta decyzja zmusiła nas do przyjęcia bardziej niezawodnego systemu komunikacji między procesami.

  • Ponieważ aktywności są w różnych procesach, nie mają wspólnej pamięci, a wiele typowych metod IPC na Androidzie nie działa lub nie spełnia wymagań.

  • Do komunikacji między główną aktywnością NBA a wbudowaną aktywnością Unity używamy języka definiowania interfejsu Androida (AIDL).

  • Komunikacja między Androidem a Unity również opiera się na interfejsach, wykorzystując wbudowaną w Unity klasę AndroidJavaProxy do rejestrowania interfejsu Kotlin w implementacji interfejsu C#.

  • Nasz moduł komunikacyjny skupia się głównie na tym, co nazywamy mostem aktywności.

    // 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) }
    }
    }
  • Obie aktywności mają dostęp do mostu aktywności i zawartych w nim metod użytkowych.

  • Most zawiera odniesienie do usługi, która implementuje zdefiniowane przez nas interfejsy AIDL. Zapewnia też łatwy sposób powiązania aktywności z procesem, przepraszam, z usługą, i zarządza wewnętrznie jej cyklem życia.

  • Symulację można rozpocząć za pomocą wywołania zwrotnego, w którym jako parametr przekazujemy identyfikator meczu.

  • Następnie mostek przekazuje go przez AIDL, a potem do aplikacji Unity, gdzie C# otrzymuje polecenie i rozpoczyna konfigurowanie sceny.

    public class NativeInterfaceListener : AndroidJavaProxy {
    [UsedImplicitly]
    public void startMatch(string matchID) {
       UnityThreadHelper.InvokeOnMain(() => _internalAPI.StartMatch(matchID));
    }
    }
  • Musimy poczekać, aż Unity będzie gotowy do naciśnięcia poleceń, które mu przekazujemy.

  • Aktywność Unity, która opakowuje aplikację, będzie aktywna, zanim Unity zostanie uruchomione.

  • Polecenia wysłane zbyt wcześnie są przechowywane w kolejce wywołań zwrotnych.

    fun onStartMatchClickedInternal() {
       if (isServiceBound) {
           AIDLBridgeService.startMatch("match_id")
       } else {
           delayedCallbacksQueue.add(action)
       }
    }
  • Po zakończeniu ładowania i inicjowania Unity, gdy stan jest bezpieczny, przekazujemy z C# informację, że Unity jest gotowe.

    // After bootload and initialization
    
    using AndroidJavaClass nativeBridge = new("com.company.UnityActivity");
    using AndroidJavaObject instance = nativeBridge.GetStatic<AndroidJavaObject>("instance");
    instance.Call("onUnityReady");
  • Następnie przechodzimy przez kolejkę i przekazujemy wszystkie zapisane polecenia.

    @Suppress("unused") //  Unity implicit call
    fun onUnityReady() {
       bridge.activityBridgeServiceInternal?.onUnityReady()
    }
    
    delayedCallbacksQueue.forEach { it(activityBridgeService) }
  • Ten diagram lepiej ilustruje, jak działa nasza komunikacja między procesami.

  • Jak widać, za pomocą mostu między aktywnościami możemy obsługiwać wspomniane wywołania zwrotne.

  • Skonfigurowaliśmy StateFlows, które są aktualizowane z Unity, aby monitorować wartości takie jak stan uwierzytelniania czy sygnatura czasowa bieżącego stanu dopasowania.

  • Zarządzamy też szyfrowaniem danych logowania i przekazujemy wszelkie błędy z Unity, aby NBA mogła łatwiej je debugować i decydować, jak najlepiej je obsługiwać na poziomie aplikacji.

  • To wszystko na ten temat.

  • Teraz oddam głos Joshowi, który opowie więcej o tym, jak zintegrowali pakiet Court View Unity z aplikacją NBA.

  • Używamy tych zależności Gradle, aby zintegrować Unity jako bibliotekę z naszą aplikacją Jetpack Compose: środowisko wykonawcze XR do uzyskiwania dostępu do sesji 3D, Jetpack SceneCore do hostowania aktywności Unity i przełączania środowisk przestrzennych oraz Jetpack Compose for XR do uzyskiwania dostępu do paneli przestrzennych w naszym kodzie Compose.

    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")
    }
  • Teraz zobaczmy, jak renderujemy widok boiska w 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)
            }
        }
    }
  • Zanim uruchomimy widok kortu, przeniesiemy Cię do trybu pełnej przestrzeni.

  • Jest to wymagane do wyświetlania panelu przestrzennego z Unity.

  • Następnie wysyłamy informacje o grze przez AIDL, aby uruchomić widok kortu w przypadku konkretnej gry.

  • Następnie uruchamiany jest silnik Unity.

  • Nasz archiwum Androida wysyła to jako intencję do aktywności Unity, którą opakowujemy w SpatialActivityPanelController.

  • Następnie przekazujemy ten kontroler do SpatialActivityPanel.

  • Ten panel zawiera widok kortu i jest udostępniany w naszej podprzestrzeni z naszym głównym panelem przestrzennym.

  • Dzięki temu możemy bez problemu sterować obydwoma urządzeniami jednocześnie.

  • To wszystko.

  • Odeślę go do Luke'a.

Podsumowanie i zasoby

  • Bardzo dziękuję.

  • Jak widać, NBA udało się przyspieszyć rozwój Court View dzięki rozdzieleniu prac nad Unity i Jetpack Compose.

  • Więcej informacji o tym, jak zacząć korzystać z kontenerów przestrzennych i używać ich we własnych aplikacjach na Androida XR, znajdziesz w dokumentacji dla deweloperów na stronie developer.android.com/develop/xr.

  • Dziękujemy za udział w dzisiejszej sesji. Nie możemy się doczekać, aby zobaczyć, co stworzycie.

Kod

Fragmenty kodu

Fragment skryptu Kotlin w 09:30 w filmie „Komunikacja między Kotlinem a Godotem”
androidResources {
     ignoreAssetsPattern = "!.svn:!.git:!.gitignore:!.ds_store:!*.scc:<dir>_*:!CVS:!*~:!android:!export_presets.cfg"
 }
Fragment skryptu Kotlin w 10:27 w filmie „Komunikacja między Kotlinem a Godotem”
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")
Fragment kodu w języku Kotlin w 10:48 w filmie „Renderowanie Godota w 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 }
    )
}
Fragment kodu w języku Kotlin w 10:55 w filmie „Rendering Godot in Jetpack Compose”
Subspace {
        SpatialPanel(
            modifier = SubspaceModifier
                .width(960.dp)
                .height(540.dp)
                .offset(z = (-200).dp)
                .movable()
,
    ) {
        GodotView(
            godotHost = godotHost,
            modifier = Modifier.fillMaxSize(),
        )
    }
}
Fragment kodu w języku Kotlin w 11:05 w filmie „Rendering Godot in 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)
       }
   }

}
Fragment kodu w Kotlinie w 11:29 w filmie „Renderowanie Godot w Jetpack Compose”
class MainActivity : ComponentActivity(), GodotHost {
    override fun getHostPlugins(engine: Godot): Set<GodotPlugin> =
        setOf(SoccerGodotPlugin(engine))
}
Fragment kodu w Kotlinie w 11:52 w filmie „Rendering Godot in 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),
    )

}
Fragment tekstu w 12:01 w filmie „Renderowanie Godot w 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)
Fragment kodu w języku Kotlin w 12:10 w filmie „Renderowanie Godot w Jetpack Compose”
class SoccerGodotPlugin(godot: Godot) : GodotPlugin(godot) {

    fun sendCommandToGodot(command: String) {
        emitSignal("match_command", command)
    }
}
Fragment tekstu w 12:20 w filmie „Renderowanie Godot w Jetpack Compose”
func _on_goal_scored(minute: int, home: int, away: int) -> void:

    if soccer_plugin:
        soccer_plugin.onMatchEvent("GOAL", minute, home, away)
Fragment kodu w Kotlinie w 12:30 w filmie „Renderowanie Godot w 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)
        }
    }
Fragment kodu w Kotlinie w 14:35 w filmie „Renderowanie Godot w 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, ...) }
}
Fragment kodu w Kotlinie w 14:57 w filmie „Rendering Godot in 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
       )
   }
Fragment kodu XML w 15:22 w filmie „Renderowanie Godota w 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="" />
Fragment skryptu Kotlin w 20:41 w filmie „Integrating Unity as a 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")
}
}
Fragment kodu w języku Kotlin w 22:11 w filmie „Integrating Unity as a 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) }
}
}
Fragment kodu w C# w 22:36 w filmie „Integrating Unity as a library”
public class NativeInterfaceListener : AndroidJavaProxy {
[UsedImplicitly]
public void startMatch(string matchID) {
   UnityThreadHelper.InvokeOnMain(() => _internalAPI.StartMatch(matchID));
}
}
Fragment kodu w Kotlinie w 22:54 w filmie „Integrating Unity as a library”
fun onStartMatchClickedInternal() {
   if (isServiceBound) {
       AIDLBridgeService.startMatch("match_id")
   } else {
       delayedCallbacksQueue.add(action)
   }
}
Fragment kodu w języku C# w 22:57 w filmie „Integrating Unity as a library”
// After bootload and initialization

using AndroidJavaClass nativeBridge = new("com.company.UnityActivity");
using AndroidJavaObject instance = nativeBridge.GetStatic<AndroidJavaObject>("instance");
instance.Call("onUnityReady");
Fragment kodu w Kotlinie w 23:06 w filmie „Integrating Unity as a library”
@Suppress("unused") //  Unity implicit call
fun onUnityReady() {
   bridge.activityBridgeServiceInternal?.onUnityReady()
}

delayedCallbacksQueue.forEach { it(activityBridgeService) }
Fragment skryptu Kotlin w 23:50 w filmie „Integrating Unity as a 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")
}
Fragment kodu w Kotlinie w 24:10 w filmie „Integrating Unity as a 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)
        }
    }
}