Android XR

XR için Jetpack Compose'da 3D motorlarla sürükleyici deneyimleri geliştirme

Hoparlörler

Hakkında

Konuşma hakkında

3D oyun motorlarını (Godot ve Unity) Jetpack Compose ile mobil ve Android XR için tek bir kod tabanında nasıl entegre edeceğinizi öğrenin. 3D sahneleri uzamsal kapsayıcılara yerleştirmeyi ve Kotlin ile motor arasındaki iletişimi ayarlamayı inceleyeceğiz. Ayrıca NBA'in Android XR'da canlı basketbol deneyimini nasıl oluşturduğuna dair özel bir bakış sunacağız.

Özet

  • OpenXR Spatial Containers uzantısı, 3D motorların yerel Android kullanıcı arayüzüyle alan paylaşmasına olanak tanır. Gerçek zamanlı motorlar, gökyüzü kutusunu devralmak yerine Jetpack XR uzamsal panelleriyle birlikte yönetilen bir 3D hacimde çalışır.
  • Geliştiriciler, gerçek zamanlı motorları doğrudan Jetpack Compose uygulamalarına AAR kitaplıkları olarak yerleştirebilir. Bu yaklaşım, motor 3D bileşenleri oluştururken yerel Android'in iş mantığını ve kullanıcı arayüzünü işlemesine olanak tanır.
  • Godot'yu yerleştirmek için bir AAR bağımlılığı ve gizli öğeleri korumak için bir AAPT derleme kuralı gerekir. Ardından, bir GodotPlugin alt sınıfı Kotlin ile çift yönlü iletişimi yönetir.
  • Compose kullanıcı arayüzü döngüleri, sürekli motor tick döngülerinden ayrı tutulmalıdır. Motor güncellemelerini körü körüne yönlendirmek, sürekli yeniden oluşturmayı tetikler ve kullanıcı arayüzünde gecikmeye neden olur.
  • NBA uygulaması, Unity'yi ayrı bir işlemde yerleşik kitaplık olarak entegre eder ve işlemler arası iletişim için AIDL kullanarak ana uygulamayı kilitlenmelere karşı korur.

Kaynaklar

Gemini Notebook

Konular

  • android-xr
  • jetpack-compose
  • uzamsal kapsayıcılar
  • openxr
  • godot
  • birlik

Transkript

Android XR'a giriş

  • Herkese merhaba.

  • Adım Luke Hopkins.

  • Android XR ekibinde geliştirici ilişkileri mühendisi olarak çalışıyorum.

  • Bugünkü konuşmamızda, Godot ve Unity gibi oyun motorlarını Android projenize nasıl yerleştirebileceğinizden bahsedeceğiz.

  • Daha sonra NBA ve Beyond Sports'tan Josh ve Rodrigo, Unity'yi NBA uygulamalarına kitaplık olarak nasıl uyguladıklarını anlatacak.

  • Bu oturumda, Godot'yu Jetpack Compose uygulamanıza nasıl entegre edeceğinizi ayrıntılı bir şekilde inceleyeceğiz. Ardından Josh ve Rodrigo, NBA uygulamalarını anlatacak.

  • Android ekosisteminin bir uzantısı olan Android XR, bildiğiniz ve sevdiğiniz deneyimleri yeni bir boyuta taşımak için çok çeşitli form faktörlerini kapsar.

  • Mevcut XR ve Android uygulamalarınızı bu yeni alana taşımayı kolaylaştırdık ve şu anda kullandığınız mevcut Android teknolojilerinden yararlanıyoruz.

  • Android XR, çok çeşitli cihazlar için tasarlanmış birleşik bir platformdur.

  • Android XR cihazlar farklı deneyim türlerini destekler.

  • Ses ve görüntülü gözlükler için artırılmış deneyimler, XR kulaklıkları ve kablolu XR gözlükleri için ise sürükleyici deneyimler sunuyoruz.

  • Bugün, kablolu XR gözlükleri ve XR kulaklıkları için sürükleyici deneyimler oluşturma konusunu ele alacağız.

  • Sürükleyici deneyimler oluşturmak için Jetpack XR SDK'mızı veya gerçek zamanlı bir motorla OpenXR kullanabilirsiniz.

  • Unity, Godot ve Unreal'ı desteklediğimiz için hangi aracı kullanırsanız kullanın Android için etkileyici deneyimler oluşturabilirsiniz.

  • Bu konuşmada, Jetpack'in SceneCore API'sini Godot ve yeni bir OpenXR uzantısıyla birlikte kullanarak bunu nasıl yapabileceğinizi açıklayacağım.

  • Burada, Godot'nun Jetpack'in uzamsal panelleriyle yan yana çalıştığını görebilirsiniz.

  • Godot'nun uzamsal içerik olarak nasıl oluşturulduğuna dikkat edin. Bu sayede, uzamsal kapsayıcıda 2D Compose kullanıcı arayüzünün yanı sıra 3D hacim sağlanır.

  • Bu yaklaşım, geliştiricilerin fizik simülasyonları ve özel gölgelendiriciler çalıştırmak için tercih ettikleri gerçek zamanlı motoru kullanmalarına olanak tanırken uygulamanızın geri kalanında yerel Android geliştirme uygulamalarını kullanmalarını sağlar.

  • Android XR ve XREAL AURA ile siz de bu tür deneyimler oluşturabilirsiniz.

  • Bu konuşmada, Godot ve Android Studio'yu kullanarak bu deneyimi nasıl oluşturacağınızı öğreneceksiniz. Ardından, NBA ve Beyond Sports'taki iş ortaklarımızdan bu deneyimi kitaplık olarak Unity'yi kullanarak nasıl oluşturduklarını dinleyeceksiniz.

Uzamsal kapsayıcılar

  • Geçmişte OpenXR ile XR deneyimi oluşturmak, her pikseli oluşturan bir deneyim oluşturmak anlamına geliyordu.

  • Bu, bir geliştiricinin ikinci bir etkinlik göstermesi gerektiğinde her ikisini aynı anda gösteremeyeceği anlamına gelir.

  • Khronos, OpenXR çalışma grubu, bu sorunu çözmek için sektörün önde gelen birkaç şirketiyle işbirliği yaparak OpenXR Spatial Containers uzantısını yayınladı.

  • Uzamsal kapsayıcıları, kutu içindeki OpenXR olarak düşünebilirsiniz.

  • Motorunuz tüm gökyüzü kutusuna sahip olmak yerine, Jetpack'in XR uzamsal panelleri, diğer uzamsal kapsayıcılar ve standart Android etkinlikleriyle yan yana bulunabilen, sistem tarafından yönetilen bir 3D hacme sahip olur.

  • Sürükleyici bir oyun oynarken aynı anda iki üç boyutlu panel göstererek Android XR'dan yararlanmak istediğinizi varsayalım.

  • Her şeyi motorda oluşturursunuz. Bu kullanım alanlarında Godot düzenleyici ile uzamsal kapsayıcının aynı anda çalıştırılması mümkün olur.

  • Google, Spatial API düzeyi 4'ten itibaren Android XR'da Spatial Container'ları kullanıma sundu.

  • Üç boyutlu kapsayıcılar iki önemli görüntüleme modunu destekler.

  • Birincisi sınırlı moddur.

  • Bu, çeşitli yerleşik kaçınma özelliklerine sahip tanımlı bir 3D hacim sağlar.

  • Kullanıcılar, uzamsal kapsayıcınızı tutup döndürebilir, konumlandırabilir, küçültebilir. Ayrıca kapsayıcı, stereo derinlik projeksiyonunu destekler. Bu sayede, kapsayıcının kenarlarını kırparken de sihirli pencere veya 3D açılır pencere efektini kullanabilirsiniz.

  • İkincisi ise sınırsız moddur.

  • Kullanıcı tam ekran deneyimi yaşamak isterse uygulamanız sınırlı moddan tam ekran alanı moduna sorunsuz bir geçiş isteyebilir. Ardından sınırlı moda geri dönebilirsiniz.

  • Android XR, üç boyutlu kapsayıcıların eklenmesiyle yeni ve kapsamlı fikirlerin önünü açıyor.

  • Android kullanıcı arayüzünüzü oyununuza taşımak veya uygulamanızda 3D içerik görüntülemek için özel bir gölgelendirici oluşturmak istediğinizi varsayalım.

  • Uzamsal kapsayıcılar kullanıma sunulmadan önce, Compose ve bir oyun motorunun aynı anda çalıştırılması mümkün değildi.

  • Gerçek zamanlı motorlarda uygulama geliştirmeye baktığımızda da iki farklı paradigmada yaşadığımızı görüyoruz.

  • Yerel Android geliştiricileri için uygulama çalışma zamanı Jetpack Compose'u temel alır.

  • Hızlı ve duyarlı kullanıcı arayüzü, pil verimliliği, dinamik düzenler, sistem erişilebilirliği ve sorunsuz metin girişi için tasarlanmıştır.

  • Bir şey olmadığında boşta kalır ve tahmin edilebilir.

  • Oyun motoru geliştiricileri için alan, Godot ve Unity gibi araçların öne çıktığı gerçek zamanlı oluşturmadır.

  • Kullanıcı etkileşimini beklemezler.

  • Sürekli bir döngü içinde çalışırlar.

  • Bu nedenle, saniyede 60, 90, 100 kare hızında olabilir.

  • Fizik hesaplamaları, karmaşık gölgelendiriciler, iskelet animasyonları ve üç boyutlu ses gibi birçok işlemi inanılmaz bir performansla gerçekleştirirler.

3D motorları entegre etme yaklaşımları

  • Peki her iki dünyanın da en iyi özelliklerini kullanmak isterseniz ne olur?

  • Önceden geliştiricilerin bu iki yaklaşımdan birini uygulaması gerekiyordu.

  • A seçeneği, uygulamanızın tamamını Godot veya Unity gibi gerçek bir oyun motorunda yeniden yazmaktır.

  • Mevcut kod tabanınızı atmak, yerel Compose kullanıcı arayüzünüzün büyük bir kısmını kaybetmek, temel metin ve gezinme özelliklerini yeniden uygulamak ve metin okuma gibi Android API'leri için sarmalayıcılar yazmak anlamına gelir.

  • B seçeneği, ham Vulkan ve OpenXR API'lerini kullanarak yerel uygulamanızda sıfırdan düşük seviyeli bir 3D grafik oluşturma ardışık düzeni oluşturmaktır.

  • Ürün özellikleri sunmak yerine aslında kendi özel motorunuzu oluşturuyorsunuz.

  • Bugün size Godot ve Unity'yi kitaplık olarak nasıl en iyi şekilde kullanabileceğinizi göstereceğim.

  • Artık uzamsal kapsayıcıların eklenmesiyle her iki dünyanın da en iyi özelliklerinden yararlanabilirsiniz.

  • Bunu, Jetpack uygulamanızdaki bir kitaplığa doğrudan gerçek zamanlı bir motor yerleştirerek yapabiliriz.

  • Üretim Android uygulama yapısını, iş mantığını ve uygulamanızdaki 3D uzamsal içeriğin sorunsuz şekilde oluşturulmasını korursunuz.

  • Bu, tüm form faktörleri için tek bir APK'nız olabileceği anlamına da gelir.

  • Yalnızca 3D içerik göstermek istemediğiniz durumlarda, kitaplığı etkinliğe veya parçaya yüklememeyi seçebilir, kod tabanınızı temiz tutabilir ve birçok cihazı destekleyebilirsiniz.

  • Felsefe basittir: Uygulamayı ayırın ve gerçek zamanlı motoru bir kitaplık haline getirin.

  • Android uygulaması veya motor arasında seçim yapmak yerine motoru, AAR bağımlılığı olarak da bilinen yerleşik bir yazılım kitaplığı olarak değerlendirirsiniz.

  • Bu kitaplığı, diğer kitaplıklarda olduğu gibi doğrudan Gradle derlemenize çekersiniz.

  • Yerel Android kodunuz, 3D bileşenleri barındıran uygulamanın mutlak beyni olmaya devam eder.

  • Jetpack Compose XR, en iyi yaptığı işi yapar: Yerel, erişilebilir ve akıcı kullanıcı arayüzünü oluşturur ve uygulama durumunu yönetir.

  • 3D öğe gerektiğinde ise uygulama, oyun motorunun kendisi tarafından oluşturulan yerleşik bir oluşturma görünümü barındırır.

  • Bugün, Godot'da bu sürecin arka planda nasıl işlediğini ayrıntılı bir şekilde inceleyeceğiz.

  • Kotlin ve GDScript arasında çift yönlü iletişim kurma ve yerleştirme mekaniğiyle başlayacağım.

  • Ardından, yeni nesil çoklu görev ve uzamsal düzenin kapılarını açacak yeni OpenXR uzantısı Spatial Container'ı inceleyeceğiz.

  • Daha önce gösterdiğim demoya kısaca göz atalım.

  • Bu oyunu Godot ve Android Studio kullanarak geliştirdim.

  • Bu klipte ise XR için Jetpack Compose'un, skor tablomuz, bildirimlerimiz ve öne çıkanlarımız için yerel 2D kullanıcı arayüzü uzamsal panellerini nasıl oluşturduğunu görebilirsiniz.

  • Ayrıca Godot, bir futbol maçının simülasyonunu oluşturuyor.

Kotlin ve Godot arasında iletişim

  • Burada, Kotlin'den GDScript'e nasıl iletişim kurduğumuzun dökümünü görebilirsiniz.

  • Godot, bu amaçla bir eklenti mimarisi sunar.

  • GodotPlugin sınıfını genişleten basit bir Kotlin sınıfı yazarsınız.

  • Bu sınıf, JVM ile Godot'un C++ çalışma zamanı arasında bir arayüz görevi görür.

  • Bu nedenle, uygulamanız ile Godot arasındaki etkileşimler çok hafiftir.

  • Godot'nun mimarisi, Godot ile Kotlin arasında iki yönlü iletişimi destekler.

  • Godot'da gol veya faul gibi bir oyun etkinliği gerçekleştiğinde Godot, Kotlin işlevlerini doğrudan çağırabilir ve Compose kullanıcı arayüzünü anlık olarak güncelleyebilir.

  • Kullanıcı, medya oynatıcıyla etkileşime geçtiğinde (ör. duraklatma veya farklı öne çıkanları seçme) Kotlin, Godot'ya bir sinyal gönderir ve ilgili oyun motoru işlemleri gerçekleşir.

  • Godot eklentisi geçildikten sonra performans ve yönetim, alan adı sınırlarına göre belirlenir.

  • Android uygulamaları ve Godot projeleri için en iyi standart uygulamaları kullanın.

  • Şimdi mimariye göz atalım.

  • Şimdi bunu Godot'da bir Android uygulamasına nasıl bağlayacağımıza bakalım.

  • Godot motoru çalışma zamanı, Maven aracılığıyla AAR olarak dağıtılır.

  • Derlemenize entegrasyon, standart Android uygulamalarını takip eder.

  • Hemen dikkat etmeniz gereken önemli bir adım var. Ben buna "öğe tuzağı" diyorum.

    androidResources {
         ignoreAssetsPattern = "!.svn:!.git:!.gitignore:!.ds_store:!*.scc:<dir>_*:!CVS:!*~:!android:!export_presets.cfg"
     }
  • Godot projelerinde sahneleriniz ve proje yapılandırma dosyalarınız bir arada bulunur.

  • Godot'yu yerleştirirken Godot projesi doğrudan öğeler klasörünüze eklenir.

  • Yani içe aktarılan tüm 3D mesh'ler, sıkıştırılmış dokular ve derlenmiş dokular, .godot adlı gizli bir dosyanın içinde çalışma zamanı yapısı olarak depolanır.

  • Varsayılan olarak Android, derleme sırasında bu klasörü kaldırır. Bu nedenle, nokta içeren tüm dizinleri yoksayar.

  • Bu nedenle, uygulamanızı normal şekilde senkronize edip derlerseniz APK derlenir ancak bu öğeler APK'da derlenmediği için kilitlenir ve yüklenir.

  • Bu sorunu düzeltmek için build.gradle dosyanızdaki AAPT seçeneklerini açıkça geçersiz kılabilir ve Godot'tan gelen baştaki noktalara izin verebilirsiniz.

  • Bunu yaptıktan sonra Godot sahnenizi Godot düzenleyicide bir kez çalıştırabilir ve ardından diğer her şeyi Android Studio'dan derleyebilirsiniz.

  • Uzamsal kapsayıcıları ve en yeni OpenXR sürümünü desteklemek için Godot 4.8 ve sonraki sürümleri, Godot tedarikçi uzantıları 5.2'yi ve Khronos OpenXR yükleyici 1.1.63'ü kullanmanızı öneririz.

    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")

Jetpack Compose'da Godot'yu oluşturma

  • Öğeler paketlendikten sonra Godot ve Compose bunları nasıl oluşturur?

  • Godot kitaplığı, onInitRenderView adlı bir fabrika yöntemi sağlar.

    @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 }
        )
    }
  • Bu, Godot'nun oluşturduğu bir SurfaceView döndürür.

  • Bu nedenle, bunu bir Composable işlevine sarmalarsınız ve Godot artık boyutlandırılabilir, doldurulabilir ve bir varlık panelinin herhangi bir yerine yerleştirilebilir.

    Subspace {
            SpatialPanel(
                modifier = SubspaceModifier
                    .width(960.dp)
                    .height(540.dp)
                    .offset(z = (-200).dp)
                    .movable()
    ,
        ) {
            GodotView(
                godotHost = godotHost,
                modifier = Modifier.fillMaxSize(),
            )
        }
    }
  • Motor, yaşam döngüsünü kendi başına yönetemez.

    @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)
           }
       }
    
    }
  • Bu nedenle, etkinliğinizin GodotHost arayüzünü uygulaması gerekir.

  • Bu sayede, etkinliğinizde gerçekleşen tüm yaşam döngüleri (duraklatma, devam ettirme) Godot motoruna gönderilir ve motor, yaşam döngüsünden haberdar olur.

  • Ardından, sorunsuz bir şekilde iletişim kurmak için yerel uygulamamızı ve motorumuzu eklememiz gerekir.

  • Bunun için GodotHost arayüzünü kullanabiliriz.

    class MainActivity : ComponentActivity(), GodotHost {
        override fun getHostPlugins(engine: Godot): Set<GodotPlugin> =
            setOf(SoccerGodotPlugin(engine))
    }
  • Godot eklentisini tanımlayabilir ve Godot başlatıldığında eklentinin Compose tarafında kaydedildiğinden emin olabiliriz.

  • Eklenti kaydedildiğine göre şimdi Android'i Godot'ya sinyal göndererek iletişim kuracak şekilde ayarlayalım.

  • Kotlin kodumuzda GodotPlugin örneği oluşturacağız.

  • Etkinliğimiz, eklentiyi Godot kitaplığına kaydedecek.

  • Kotlin'de ise GodotPlugin oluşturup adını kaydediyor ve Godot'ya bu pakette kullanmak istediğimiz tüm sinyalleri bildiriyoruz.

    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),
        )
    
    }
  • Sırada, bu eklentinin örneğini alabileceğiniz bir Godot komut dosyası var.

    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)
  • Nelerin kaydedildiğini görebilir ve ardından işlevleri kaydettiğimiz sinyallere bağlayabiliriz.

  • emitSignal gibi bir şeyi çağırdıktan sonra bunu Kotlin kodunuzdan iletebilirsiniz. Bu kod doğrudan Godot'ya gider.

    class SoccerGodotPlugin(godot: Godot) : GodotPlugin(godot) {
    
        fun sendCommandToGodot(command: String) {
            emitSignal("match_command", command)
        }
    }
  • Godot'nun Kotlin ile iletişim kurması için (örneğin, bir gol atıldığında) doğrudan Godot komut dosyasından bir Kotlin işlevini çağırabiliriz. Bunun için eklenti örneğini kullanmamız yeterlidir.

    func _on_goal_scored(minute: int, home: int, away: int) -> void:
    
        if soccer_plugin:
            soccer_plugin.onMatchEvent("GOAL", minute, home, away)
  • İlk olarak Kotlin işlevine UsedByGodot ile ek açıklama ekleyin.

    @UsedByGodot
    fun onMatchEvent(type: String, minute: Int, home: Int, away: Int, title: String) {
    
            _uiState.update { state ->
                state.copy(homeScore = home, awayScore = away, matchSeconds = minute)
            }
        }
  • Motor, JNI sıralamasını arka planda gerçekleştirir.

  • Ardından işlevi doğrudan Godot Script'ten çağırabiliriz.

  • Bu özelliği kullanırken unutmamanız gereken bir ipucu var: UsedByGodot için tam dize eşleşmesi gerekir.

  • Bu nedenle, Godot komut dosyanız onMatchEvent yöntemini çağırıyorsa Kotlin adınızın da tam olarak aynı olması gerekir. Otomatik camel case veya snake case dönüşümü yapılmaz.

  • Bu nedenle, her iki yöntemin de tamamen aynı olduğundan emin olmanız gerekir.

  • Eklenti bağlandıktan sonra bu konuşmanın en önemli kısmı, zihinsel model olarak adlandırdığım kısımdır.

  • Oyun motorları ve Jetpack Compose, bu noktada iki farklı mimariyle çalışır.

  • Oyun motoru, sürekli kare tikleri üzerinde çalışır.

  • Herhangi bir değişiklik yapıp yapmadığınıza bakılmaksızın, oyun motoru tick işlemi saniyede 60, 90, 100 kez gerçekleşerek zengin ve sorunsuz bir gerçek zamanlı görüntü oluşturma deneyimi sunar.

  • Godot gibi motorlar, GPU ve CPU döngülerini etkili bir şekilde kullanır.

  • Jetpack Compose ise işlemleri reaktif bir şekilde gerçekleştirir ve duruma dayalıdır.

  • Uygulama değişmediğinde ise Compose devam eder ve sıfır CPU tüketir.

  • Net bir strateji olmadan iki çalışma zamanını çalıştırmak için bunları bağlamak isterseniz bu felsefeler birbiriyle çelişebilir.

  • Şimdi bu sorunu nasıl çözebileceğimize bakalım. Ben buna "tick sızıntısı" diyorum.

  • Burada 3D deneyimimiz var. Örneğin, skor tablosu zamanlayıcısı.

  • Oyun motoru komut dosyanızda sürekli değişen bir ti- timer değişkeni var.

  • Bu değişkeni Compose'a saniyede 90 kez iletirseniz bu yeniden oluşturma döngüsüne saniyede 90 kez girersiniz.

  • Bu, Compose'un saniyede 90 kez composable'larınızı yeniden değerlendireceği, düzenleri yeniden ölçeceği ve kullanıcı arayüzü ağaçlarını yeniden çizeceği anlamına gelir.

  • Kare düşüşleri ve kullanıcı arayüzü gecikmeleri yaşarsınız ve cihazınız kısa sürede ısınır.

  • Altın kural, sürekli motor döngüsünün reaktif kullanıcı arayüzü döngüsünü belirlemesine asla izin vermemektir.

  • Bu örnekte, Godot'nun skor tablosu zamanlayıcısını sürekli güncellemesi yerine Android'in bu verileri her saniye bir kez çekmesini sağlıyorum.

    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, ...) }
    }
  • Son olarak, ileti dizisi sınırını her zaman göz önünde bulundurun.

  • Godot oyun motoru döngüsü, kendi oluşturma iş parçacığında sürekli olarak çalışır.

  • Kullanıcı arayüzü, Android kullanıcı arayüzleri Android ana iş parçacığında yürütülmelidir.

  • Bu nedenle, görünüm modeli durumlarınızı güncellemeden önce Kotlin eklentinizin her zaman Dispatchers.Main veya güvenli durum temel öğelerini kullanarak etkinlikleri gönderdiğinden emin olun.

    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
           )
       }
  • Yüksek frekanslı işleri motorda ve ayrı semantik öğeleri Compose'da tutarak yerel uygulamanızın pil ömründen, erişilebilirliğinden veya performansından ödün vermeden son derece akıcı 3D görüntü alanları elde edebilirsiniz.

  • Şimdi, Godot projenize nasıl Spatial Container ekleyebileceğinizi inceleyelim.

  • Öncelikle uygulamayı Tam Ekran Alanı olarak ayarlayalım, sınırlayıcı ipuçlarıyla birlikte üç boyutlu kapsayıcıları etkinleştirelim ve ardından Android manifest dosyasında XR yükleyici kitaplığını ayarlayalım.

    <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'nun kendi XR proje ayarları var. Bu ayarlar sayesinde OpenXR, XR gölgelendiricileri ve uzamsal kapsayıcıları doğrudan düzenleyicisinden etkinleştirebiliyorum.

  • Godot'da Spatial Containers'ı etkinleştirmek için yapmanız gerekenler bu kadar.

  • Godot'yu Android XR ve üç boyutlu kapsayıcılarla çalıştırdığınıza ve üç boyutlu kapsayıcıları, üç boyutlu panelleri ve JXR'yi kullanmayla ilgili en iyi uygulamaları öğrendiğinize göre geliştirici web sitesindeki diğer bilgileri incelemenizi öneririz.

  • Godot Engine ve Android XR'da Spatial Containers'ın nasıl kullanılacağına dair bir örnek inceledik. Birçoğunuzun, Android XR deneyimi için Spatial Containers üzerinde aktif olarak çalışan bir geliştiriciden bilgi almak istediğinden eminim.

  • Şimdi de NBA'den Josh ve Rodrigo'yu davet etmek istiyorum. NBA, uygulamalarında uzamsal kapsayıcıları kullanan ilk iş ortaklarımızdan biri.

NBA Court View'a genel bakış

  • Teşekkürler Levent.

  • Merhaba,

  • Adım Josh. NBA'de Android geliştirici olarak çalışıyorum.

  • Bugün yanımda Beyond Sports'ta Unity yazılım geliştiricisi olarak çalışan meslektaşım Rodrigo Sanchez var.

  • Bugün, NBA uygulaması için Court View'u oluşturmak üzere Unity'yi kitaplık olarak nasıl kullandığımızı göstereceğiz.

  • NBA olarak, taraftarlarımıza her zaman yeni deneyimler sunmaya çalışıyoruz.

  • Bu nedenle, Google ve Beyond Sports ile birlikte Court View'u kullanıma sunmaktan heyecan duyuyoruz.

  • Saha Görünümü, NBA maçlarının 3D canlandırmalarını sunarak League Pass abonelerinin normal yayın videosuyla mümkün olmayan yeni perspektiflerden izlemesine olanak tanır.

  • Kortu ölçeklendirip yeniden döndürerek alanınızda istediğiniz yere yerleştirin.

  • Etrafında hareket edin ve maçı benzersiz açılardan izleyin.

  • Bu sırada NBA uygulamanızı normal şekilde kullanabilir, senkronize edilen istatistikleri görüntüleyebilir ve canlı yayını izleyebilirsiniz.

  • How's this all set up?

  • Saha Görünümü, Unity'de oluşturulur ve Android uygulamamız tarafından Unity kitaplık olarak kullanılarak kullanılır.

  • Ardından, NBA uygulamasının tek bir bağımlılık olarak kullanabileceği, Maven Central'da barındırılan tek bir Android Arşivi oluşturuyoruz.

  • Bu sayede işi Android ve Unity tarafı arasında bölebildik.

  • Böylece her birimiz en iyi bildiğimiz şeye odaklanabilir ve genel olarak geliştirme sürecini hızlandırmak için paralel olarak çalışabiliriz.

  • Örneğin, ekibimiz Compose'da tam alan deneyimi üzerinde çalışırken Unity ekibi 3D sahanın ölçeklendirilmesine ve düzenine odaklanabiliyordu.

  • Şimdi sözü Rodrigo'ya bırakıyorum. Unity projesi ve Unity as a Library ile nasıl çalıştıkları hakkında daha fazla bilgi verecek.

Unity'yi kitaplık olarak entegre etme

  • Teşekkürler Josh.

  • Beyond Sports olarak, maçlardan alınan gerçek verilerle 3D görselleştirmeler oluşturmak için birkaç yıldır NBA ile birlikte çalışıyoruz.

  • Bu projede, 3D simülasyonumuzu Android XR ile uyumlu olacak şekilde uyarladık.

  • Simülasyonumuz, C# ile Unity motoru üzerinde oluşturulmuştur.

  • Simülasyonu Unity ile Android'e kitaplık olarak aktarıyoruz ve Kotlin kullanarak yanında bir iletişim katmanı oluşturuyoruz.

  • NBA'in uygulamasına dahil etmesi için bu iki modülü tek bir Maven bağımlılığı olarak bir araya getirmeden önce.

  • Ekranda, Josh'un daha önce gösterdiği simülasyonun editörde çalışan kısa bir videosunu görebilirsiniz.

  • Bunu yapmak için Unity için Android XR Extensions paketini kullanırız.

  • Süreci size anlatayım.

  • Bu nedenle, Unity'de derleme yapmadan önce doğru ayarları ve eklentileri seçtiğinizden emin olun.

  • Unity'nin XR Plug-in Management paketini ve Google'ın Unity için Android XR Extensions paketini indirin.

  • OpenXR Android XR Extensions sağlayıcısını kullanmak için bunları XR Plug-in Management ayarlarında yapılandırın.

  • Kullanım alanımızda Spatial Containers'dan yararlanmak istediğimiz için bu özelliği etkinleştirdik ve oturum yönetimi ayarlarından sürükleyici XR'yi devre dışı bıraktık.

  • Tüm girişler Unity dışında yerel olarak işlendiğinden XR giriş paketi kullanmadık.

  • Ardından, oynatıcı ayarlarında projenizi minimum API düzeyi 24 veya daha yüksek bir sürümü kullanacak şekilde ayarlamanız ve uygulama giriş noktasını daha yeni Game Activity'ye ayarlamanız gerekir.

  • Etkinlik'in işaretlenmemesi gerektiğini unutmayın.

  • Yalnızca ARM64 için derleme yapmak istediğimizden bu hedef mimariyi seçiyoruz. Çözünürlük bölümünde, Arka Planda Çalıştır'ı etkinleştiriyoruz. Böylece diğer uygulamalarla çoklu görev yapabilir ve 3D simülasyonu arka planda çalıştırabilirsiniz.

  • Sistem izin istekleri gibi iletişim kutularını oluşturmak için gerekli olduğundan Resizeable Activity'yi de etkinleştiririz.

  • Son olarak, Android XR uygulamalarının Universal Render Pipeline ve Vulkan Graphics API kullanılarak oluşturulması önerilir.

  • URP öğenizde HDR ve son işleme devre dışı bırakılmalıdır.

  • Tüm bu adımları tamamladıktan sonra derleme oluşturmaya hazırsınız demektir.

  • Derleme penceresine gidin. Unity 6.1 veya sonraki bir sürümü kullanıyorsanız Android XR derleme profilini seçebilirsiniz. Bu profil, Android mobil için de derleme yapıyorsanız ayarları ayırmanıza olanak tanır.

  • Derlemeden önce projeyi dışa aktarmayı etkinleştirdiğinizden emin olun.

  • Unity derleme işlemini tamamladıktan sonra, Android Studio'da birleştirilebilen ve diğer Unity APK'ları gibi bir cihazda çalıştırılabilen çalışan bir Android projeniz olur.

  • Android Studio'da, dışa aktarılan derlemenizi başka bir uygulama tarafından kullanılacak bir kitaplığa dönüştürmeniz gerekir.

    // 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")
    }
    }
  • Buradaki projemizde dört farklı modülümüz var: NBA'in sarmalama uygulaması için bir maket yer tutucu olarak test edilecek ana APK modülü, dışa aktarılan derlememizi barındıran Unity Kitaplığı modülü, etkinlikler arasında oluşturduğumuz iletişim katmanımızı barındıran bir nativeCommunications modülü ve AAR'lerimizi Maven'a teslim etmek üzere tek bir pakette birleştirmek için kullandığımız bir packageExporter modülü.

  • Birden fazla AAR'yi birleştirmek için Android Fused Library veya Fat AAR gibi daha eski topluluk yöntemleri kullanılabilir.

  • Unity'yi ve ana etkinliği aynı işlemde çalıştırarak yapınızı ve iletişiminizi basit tutabilirsiniz.

  • Bunu yapmamaya karar verdik ve Unity etkinliğini kendi sürecine ayırdık. Böylece Unity bölümü kilitlenirse NBA uygulaması etkilenmeyecek ve kilitlenmeden Unity'yi güvenli bir şekilde kapatabilecek.

  • Bu karar, daha sağlam bir süreçler arası iletişim sistemi kullanmamızı gerektiriyor.

  • Etkinlikler farklı işlemlerde olduğundan aralarında paylaşılan bellek yoktur ve birçok yaygın Android IPC yöntemi çalışmaz veya gereksinimleri karşılamaz.

  • NBA'in ana etkinliği ile yerleşik Unity etkinliğimiz arasında iletişim kurmak için Android Arayüz Tanımlama Dili'ni (AIDL) kullanıyoruz.

  • Ardından, Android ile Unity arasındaki iletişim de arayüzlere dayalıdır. Unity'deki yerleşik AndroidJavaProxy kullanılarak bir Kotlin arayüzü, C# arayüzü uygulamasına kaydedilir.

  • İletişim modülümüz, öncelikle etkinlik köprüsü olarak adlandırdığımız şeye odaklanır.

    // 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) }
    }
    }
  • Her iki etkinlik de etkinlik köprüsüne ve içindeki yardımcı yöntemlere erişebilir.

  • Köprü, tanımladığımız AIDL arayüzlerini uygulayan bir hizmete referans içerir. Ayrıca etkinliklerin işleme (pardon, hizmete) bağlanması için kolay bir yol sağlar ve hizmetin yaşam döngüsünü dahili olarak yönetir.

  • Simülasyonu başlatmak için eşleşme kimliğini parametre olarak ilettiğimiz bir geri çağırma işlevi kullanabiliriz.

  • Ardından köprü, bu bilgiyi AIDL üzerinden Unity uygulamasına iletir. Burada C# bir komut alır ve sahneyi yapılandırmaya başlar.

    public class NativeInterfaceListener : AndroidJavaProxy {
    [UsedImplicitly]
    public void startMatch(string matchID) {
       UnityThreadHelper.InvokeOnMain(() => _internalAPI.StartMatch(matchID));
    }
    }
  • Unity'nin etkinleşmesini ve ilettiğimiz komutları uygulamaya hazır olmasını beklememiz gerekir.

  • Uygulamayı sarmalayan Unity etkinliği, Unity'den önce etkin olur.

  • Çok erken gönderilen komutları bir geri çağırma sırasında depolarız.

    fun onStartMatchClickedInternal() {
       if (isServiceBound) {
           AIDLBridgeService.startMatch("match_id")
       } else {
           delayedCallbacksQueue.add(action)
       }
    }
  • Unity, önyükleme ve başlatma işlemlerini tamamladıktan ve güvenli olduğunu düşündüğümüz bir duruma ulaştıktan sonra C# üzerinden Unity'nin hazır olduğunu bildiririz.

    // After bootload and initialization
    
    using AndroidJavaClass nativeBridge = new("com.company.UnityActivity");
    using AndroidJavaObject instance = nativeBridge.GetStatic<AndroidJavaObject>("instance");
    instance.Call("onUnityReady");
  • Ardından, sıramızda döngü oluşturur ve depolanan tüm komutlarımızı iletiriz.

    @Suppress("unused") //  Unity implicit call
    fun onUnityReady() {
       bridge.activityBridgeServiceInternal?.onUnityReady()
    }
    
    delayedCallbacksQueue.forEach { it(activityBridgeService) }
  • Bu diyagram, süreçler arası iletişimimizin nasıl çalıştığını daha iyi gösterir.

  • Gördüğünüz gibi, etkinlikler arasındaki köprüyü kullanarak bahsettiğimiz geri çağırmaları işleyebiliriz.

  • Kimlik doğrulama durumu veya maçın geçerli durum zaman damgası gibi değerleri izlemek için Unity'den güncellenen StateFlow'lar oluşturduk.

  • Ayrıca, kimlik bilgilerinin şifrelemesini de yönetiyoruz ve NBA'in daha kolay hata ayıklayabilmesi ve bunları uygulama düzeyinde en iyi şekilde nasıl ele alacağına karar verebilmesi için Unity'den gelen hataları iletiyoruz.

  • Benim söyleyeceklerim bu kadar.

  • Şimdi sözü tekrar Josh'a bırakıyorum. Court View Unity paketini NBA uygulamasına nasıl entegre ettikleri hakkında daha fazla bilgi verecek.

  • Unity'yi Jetpack Compose uygulamamıza kitaplık olarak entegre etmek için şu Gradle bağımlılıklarını kullanırız: 3D oturuma erişmek için XR Runtime, Unity etkinliğimizi barındırmak ve uzamsal ortamları açıp kapatmak için Jetpack SceneCore ve Compose kodumuzdaki uzamsal panellere erişmek için XR için Jetpack 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")
    }
  • Şimdi de Jetpack Compose'da Court View'u nasıl oluşturduğumuza bakalım.

    // 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)
            }
        }
    }
  • Saha Görünümü'nü başlatmadan önce sizi tam ekran alanına yönlendiririz.

  • Bu, Unity'yi barındıran uzamsal paneli görüntülemek için gereklidir.

  • Ardından, belirli bir oyun için Court View'u başlatmak üzere AIDL üzerinden oyun bilgilerini göndeririz.

  • Ardından Unity başlatılır.

  • Android arşivimiz bunu bir Unity etkinliğine intent olarak gönderir ve biz de bunu bir SpatialActivityPanelController'a sarmalarız.

  • Buradan, bu denetleyiciyi bir SpatialActivityPanel öğesine iletiriz.

  • Bu panel, Court View'u barındırır ve ana uzamsal panelimizle birlikte alt uzayımızda paylaşılır.

  • Bu sayede her ikisini de aynı anda sorunsuz bir şekilde kontrol edebiliyoruz.

  • Hepsi bu kadar.

  • Levent'e geri göndereceğim.

Sonuç ve kaynaklar

  • Teşekkür ederiz.

  • Gördüğünüz gibi NBA, Unity ve Jetpack Compose geliştirmelerini ayırarak Court View'u hızlandırabildi.

  • Uzamsal kapsayıcıları kullanmaya başlama ve kendi Android XR uygulamalarınızda kullanma hakkında daha fazla bilgi edinmek için developer.android.com/develop/xr adresindeki geliştirici belgelerine göz atın.

  • Bugün bize katıldığınız için çok teşekkür ederiz. Neler geliştireceğinizi görmek için sabırsızlanıyoruz.

Kod

Kod snippet'leri

"Communication between Kotlin and Godot" (Kotlin ve Godot Arasındaki İletişim) videosunda 09:30'daki Kotlin Script snippet'i
androidResources {
     ignoreAssetsPattern = "!.svn:!.git:!.gitignore:!.ds_store:!*.scc:<dir>_*:!CVS:!*~:!android:!export_presets.cfg"
 }
"Communication between Kotlin and Godot" videosunda 10:27'deki Kotlin Script snippet'i
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")
"Rendering Godot in Jetpack Compose" (Jetpack Compose'da Godot'yu Oluşturma) videosunda 10:48'deki Kotlin snippet'i
@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 }
    )
}
"Rendering Godot in Jetpack Compose" videosunda 10:55'teki Kotlin snippet'i
Subspace {
        SpatialPanel(
            modifier = SubspaceModifier
                .width(960.dp)
                .height(540.dp)
                .offset(z = (-200).dp)
                .movable()
,
    ) {
        GodotView(
            godotHost = godotHost,
            modifier = Modifier.fillMaxSize(),
        )
    }
}
"Rendering Godot in Jetpack Compose" (Jetpack Compose'da Godot'yu Oluşturma) videosunda 11:05'teki Kotlin snippet'i
@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)
       }
   }

}
"Rendering Godot in Jetpack Compose" (Jetpack Compose'da Godot'yu Oluşturma) videosunda 11:29'daki Kotlin snippet'i
class MainActivity : ComponentActivity(), GodotHost {
    override fun getHostPlugins(engine: Godot): Set<GodotPlugin> =
        setOf(SoccerGodotPlugin(engine))
}
"Rendering Godot in Jetpack Compose" (Jetpack Compose'da Godot'yu Oluşturma) videosunda 11:52'deki Kotlin snippet'i
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),
    )

}
"Rendering Godot in Jetpack Compose" videosunda 12:01'deki metin snippet'i
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)
"Rendering Godot in Jetpack Compose" (Jetpack Compose'da Godot'yu Oluşturma) videosunda 12:10'daki Kotlin snippet'i
class SoccerGodotPlugin(godot: Godot) : GodotPlugin(godot) {

    fun sendCommandToGodot(command: String) {
        emitSignal("match_command", command)
    }
}
"Rendering Godot in Jetpack Compose" videosunda 12:20'deki metin snippet'i
func _on_goal_scored(minute: int, home: int, away: int) -> void:

    if soccer_plugin:
        soccer_plugin.onMatchEvent("GOAL", minute, home, away)
"Rendering Godot in Jetpack Compose" (Jetpack Compose'da Godot'yu Oluşturma) videosunda 12:30'daki Kotlin snippet'i
@UsedByGodot
fun onMatchEvent(type: String, minute: Int, home: Int, away: Int, title: String) {

        _uiState.update { state ->
            state.copy(homeScore = home, awayScore = away, matchSeconds = minute)
        }
    }
"Rendering Godot in Jetpack Compose" videosunda 14:35'teki Kotlin snippet'i
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, ...) }
}
"Rendering Godot in Jetpack Compose" videosunda 14:57'deki Kotlin snippet'i
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
       )
   }
"Rendering Godot in Jetpack Compose" (Jetpack Compose'da Godot'yu Oluşturma) videosunda 15:22'deki XML snippet'i
<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="" />
"Integrating Unity as a library" videosunda 20:41'deki Kotlin Script snippet'i
// 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")
}
}
"Integrating Unity as a library" (Unity'yi kitaplık olarak entegre etme) videosunda 22:11'deki Kotlin snippet'i
// 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) }
}
}
"Integrating Unity as a library" (Unity'yi kitaplık olarak entegre etme) videosunda 22:36'daki C# snippet'i
public class NativeInterfaceListener : AndroidJavaProxy {
[UsedImplicitly]
public void startMatch(string matchID) {
   UnityThreadHelper.InvokeOnMain(() => _internalAPI.StartMatch(matchID));
}
}
"Integrating Unity as a library" (Unity'yi kitaplık olarak entegre etme) videosunda 22:54'teki Kotlin snippet'i
fun onStartMatchClickedInternal() {
   if (isServiceBound) {
       AIDLBridgeService.startMatch("match_id")
   } else {
       delayedCallbacksQueue.add(action)
   }
}
"Integrating Unity as a library" (Unity'yi kitaplık olarak entegre etme) videosunda 22:57'deki C# snippet'i
// After bootload and initialization

using AndroidJavaClass nativeBridge = new("com.company.UnityActivity");
using AndroidJavaObject instance = nativeBridge.GetStatic<AndroidJavaObject>("instance");
instance.Call("onUnityReady");
"Integrating Unity as a library" (Unity'yi kitaplık olarak entegre etme) videosunda 23:06'daki Kotlin snippet'i
@Suppress("unused") //  Unity implicit call
fun onUnityReady() {
   bridge.activityBridgeServiceInternal?.onUnityReady()
}

delayedCallbacksQueue.forEach { it(activityBridgeService) }
"Integrating Unity as a library" (Unity'yi kitaplık olarak entegre etme) videosunda 23:50'deki Kotlin komut dosyası snippet'i
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")
}
"Integrating Unity as a library" (Unity'yi kitaplık olarak entegre etme) videosunda 24:10'daki Kotlin snippet'i
// 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)
        }
    }
}