Android XR

Jetpack Compose for XR में 3D इंजन की मदद से, इमर्सिव एक्सपीरियंस को बेहतर बनाना

स्पीकर

  • ल्यूक हॉपकिंस

    Android XR डेवलपर रिलेशंस इंजीनियर

  • जोशुआ लॉरेट

    Android स्टाफ़ इंजीनियर

  • रॉड्रिगो सांचेज़

    सॉफ़्टवेयर डेवलपर - Beyond Sports

इसके बारे में

बातचीत के बारे में जानकारी

Jetpack Compose के साथ 3D गेम इंजन (Godot और Unity) को इंटिग्रेट करने का तरीका जानें. इससे मोबाइल और Android XR के लिए एक ही कोड बेस बनाया जा सकता है. हम स्पेशल कंटेनर में 3D सीन एंबेड करने और Kotlin-to-engine कम्यूनिकेशन सेट अप करने के बारे में जानेंगे. साथ ही, हम यह भी देखेंगे कि एनबीए ने Android XR पर लाइव बास्केटबॉल का अनुभव कैसे तैयार किया.

खास जानकारी

  • OpenXR Spatial Containers एक्सटेंशन की मदद से, 3D इंजन, नेटिव Android यूज़र इंटरफ़ेस (यूआई) के साथ स्पेस शेयर कर सकते हैं. रीयल-टाइम इंजन, स्काईबॉक्स को कंट्रोल करने के बजाय, Jetpack XR के स्पेशल पैनल के साथ-साथ मैनेज किए गए 3D वॉल्यूम में चलते हैं.
  • डेवलपर, रीयल-टाइम इंजन को सीधे Jetpack Compose ऐप्लिकेशन में एएआर लाइब्रेरी के तौर पर एम्बेड कर सकते हैं. इस तरीके से, नेटिव Android, कारोबारी नियम और यूज़र इंटरफ़ेस (यूआई) को मैनेज करता है. वहीं, इंजन 3D कॉम्पोनेंट को रेंडर करता है.
  • Godot को एम्बेड करने के लिए, एएआर डिपेंडेंसी और एएपीटी बिल्ड नियम की ज़रूरत होती है, ताकि छिपी हुई ऐसेट को सेव रखा जा सके. इसके बाद, GodotPlugin सबक्लास, Kotlin के साथ दोनों तरफ़ से होने वाले कम्यूनिकेशन को मैनेज करता है.
  • Compose यूज़र इंटरफ़ेस (यूआई) लूप, लगातार चलने वाले इंजन टिक लूप से अलग होने चाहिए. इंजन के अपडेट को बिना सोचे-समझे फ़ॉरवर्ड करने से, बार-बार रीकंपोज़िशन ट्रिगर होता है. इससे यूज़र इंटरफ़ेस (यूआई) में रुकावट आती है.
  • NBA ऐप्लिकेशन, Unity को एक अलग प्रोसेस में एम्बेड की गई लाइब्रेरी के तौर पर इंटिग्रेट करता है. इसके लिए, वह इंटर-प्रोसेस कम्यूनिकेशन के लिए AIDL का इस्तेमाल करता है. इससे होस्ट ऐप्लिकेशन को क्रैश होने से बचाया जा सकता है.

संसाधन

Gemini Notebook

विषय

  • android-xr
  • jetpack-compose
  • स्पेशल-कंटेनर
  • openxr
  • godot
  • unity

ट्रांसक्रिप्ट

ट्रांसक्रिप्ट डाउनलोड करें

Android XR के बारे में जानकारी

  • सभी को नमस्ते।

  • मेरा नाम ल्यूक हॉपकिंस है.

  • मैं Android XR टीम में डेवलपर रिलेशंस इंजीनियर हूं.

  • आज की बातचीत में, हम इस बारे में बात करेंगे कि Godot और Unity जैसे गेम इंजन को अपने Android प्रोजेक्ट में कैसे एम्बेड किया जा सकता है.

  • इसके बाद, एनबीए और बियॉन्ड स्पोर्ट्स के जोश और रॉड्रिगो हमारे साथ जुड़ेंगे. वे बताएंगे कि उन्होंने अपने एनबीए ऐप्लिकेशन में, Unity को लाइब्रेरी के तौर पर कैसे लागू किया है.

  • इस सेशन में, हम Godot को Jetpack Compose ऐप्लिकेशन में इंटिग्रेट करने के बारे में जानेंगे. इसके बाद, जोश और रॉड्रिगो अपने एनबीए ऐप्लिकेशन के बारे में बताएंगे.

  • Android XR, Android ईकोसिस्टम का ही एक हिस्सा है. यह अलग-अलग तरह के डिवाइसों पर काम करता है, ताकि आपको अपने पसंदीदा ऐप्लिकेशन और सुविधाओं का एक नया और बेहतर अनुभव मिल सके.

  • हमने आपके मौजूदा एक्सआर और Android ऐप्लिकेशन को इस नए प्लैटफ़ॉर्म पर लाना आसान बना दिया है. साथ ही, हम उन मौजूदा Android टेक्नोलॉजी का इस्तेमाल कर रहे हैं जिनका इस्तेमाल करके, फ़िलहाल ऐप्लिकेशन बनाए जा रहे हैं.

  • Android XR एक यूनिफ़ाइड प्लैटफ़ॉर्म है. इसे कई तरह के डिवाइसों के लिए डिज़ाइन किया गया है.

  • Android XR डिवाइसों पर, अलग-अलग तरह के अनुभव मिलते हैं.

  • हमारे पास ऑडियो और डिसप्ले ग्लास के लिए, ऑगमेंटेड एक्सपीरियंस उपलब्ध हैं. इसके अलावा, हमारे पास एक्सआर हेडसेट और वायर वाले एक्सआर के स्मार्ट ग्लास के लिए, इमर्सिव एक्सपीरियंस उपलब्ध हैं.

  • आज हम वायर वाले एक्सआर के स्मार्ट ग्लास और एक्सआर हेडसेट के लिए, इमर्सिव अनुभव तैयार करने के बारे में बात करेंगे.

  • इमर्सिव एक्सपीरियंस बनाने के लिए, Jetpack XR SDK का इस्तेमाल किया जा सकता है. इसके अलावा, रीयल-टाइम इंजन के साथ OpenXR का इस्तेमाल किया जा सकता है.

  • हम Unity, Godot, और Unreal के साथ काम करते हैं, ताकि आप Android के लिए बेहतरीन अनुभव तैयार कर सकें. इससे कोई फ़र्क़ नहीं पड़ता कि आपने कौनसा टूल इस्तेमाल किया है.

  • इस बातचीत में, मैं आपको Jetpack के SceneCore एपीआई के साथ-साथ Godot और नए OpenXR एक्सटेंशन का इस्तेमाल करने का तरीका बताऊंगा, ताकि ऐसा किया जा सके.

  • यहां आपको Jetpack के स्पेशल पैनल के साथ-साथ Godot चलता हुआ दिख रहा है.

  • ध्यान दें कि Godot को स्पेशल कॉन्टेंट के तौर पर कैसे रेंडर किया जाता है. इससे स्पेशल कंटेनर में 2D Compose यूज़र इंटरफ़ेस (यूआई) के साथ-साथ 3D वॉल्यूम मिलता है.

  • इस तरीके से, डेवलपर अपनी पसंद के रीयल-टाइम इंजन का इस्तेमाल करके, फ़िज़िक्स सिम्युलेशन और कस्टम शेडर चला सकते हैं. साथ ही, ऐप्लिकेशन के बाकी हिस्सों के लिए नेटिव Android डेवलपमेंट के तरीकों को लागू कर सकते हैं.

  • Android XR और XREAL AURA की मदद से, आप भी इस तरह के अनुभव तैयार कर सकते हैं.

  • इस सेशन में, हम आपको Godot और Android Studio का इस्तेमाल करके, इस तरह का अनुभव तैयार करने का तरीका बताएंगे. इसके बाद, NBA और Beyond Sports के हमारे पार्टनर आपको बताएंगे कि उन्होंने Unity को लाइब्रेरी के तौर पर इस्तेमाल करके, इसे कैसे तैयार किया.

स्पेशल कंटेनर

  • पहले, OpenXR के साथ एक्सआर अनुभव बनाने का मतलब था कि हर पिक्सल को रेंडर करने वाला अनुभव बनाना.

  • इसका मतलब है कि अगर किसी डेवलपर को दूसरी गतिविधि दिखानी है, तो वह दोनों गतिविधियां एक साथ नहीं दिखा सकता.

  • इस समस्या को हल करने के लिए, OpenXR के वर्किंग ग्रुप Khronos ने इंडस्ट्री के कई लीडर के साथ मिलकर, OpenXR Spatial Containers एक्सटेंशन रिलीज़ किया है.

  • स्पेशल कंटेनर को एक बॉक्स में OpenXR की तरह समझें.

  • आपके इंजन को पूरे स्काईबॉक्स का ऐक्सेस देने के बजाय, उसे सिस्टम से मैनेज किया जाने वाला 3D वॉल्यूम दिया जाता है. यह Jetpack के XR स्पेशल पैनल, अन्य स्पेशल कंटेनर, और स्टैंडर्ड Android गतिविधियों के साथ काम कर सकता है.

  • मान लें कि आपको Android XR का इस्तेमाल करके, एक साथ दो स्पेशल पैनल दिखाने हैं. साथ ही, आपको इमर्सिव गेम भी चलाना है.

  • इंजन में सब कुछ रेंडर किया जाता है. इससे, इन इस्तेमाल के उदाहरणों में Godot एडिटर और स्पेशल कंटेनर को एक साथ चलाया जा सकता है.

  • स्पेशल एपीआई लेवल 4 से, Google ने Android XR में स्पेशल कंटेनर लागू किए हैं.

  • स्पेशल कंटेनर, दो अहम डिसप्ले मोड के साथ काम करते हैं.

  • पहला है बाउंडेड मोड.

  • इससे 3D वॉल्यूम तय किया जाता है. इसमें पहले से मौजूद कई चीज़ों को शामिल नहीं किया जाता.

  • उपयोगकर्ता आपके स्पेशल कंटेनर को पकड़ सकता है, उसे घुमा सकता है, उसकी पोज़िशन बदल सकता है, उसे पकड़ सकता है, और उसे छोटा कर सकता है. साथ ही, यह स्टीरियो डेप्थ प्रोजेक्शन के साथ काम करता है. इससे आपको वह मैजिक विंडो या 3D पॉप-आउट इफ़ेक्ट मिलता है जो कंटेनर के किनारों को क्लिप करते समय मिलता है.

  • दूसरा है अनबाउंडेड मोड.

  • अगर उपयोगकर्ता को इमर्सिव मोड का इस्तेमाल करना है, तो आपका ऐप्लिकेशन बाउंडेड मोड से फ़ुल स्पेस मोड में आसानी से ट्रांज़िशन करने का अनुरोध कर सकता है. इसके बाद, बाउंडेड मोड में वापस भी जाया जा सकता है.

  • स्पेशल कंटेनर की मदद से, Android XR नए और बड़े आइडिया के लिए रास्ते खोलता है.

  • मान लें कि आपको अपने गेम में Android यूज़र इंटरफ़ेस (यूआई) को शामिल करना है या अपने ऐप्लिकेशन में 3D कॉन्टेंट दिखाने के लिए, कस्टम शेडर बनाना है.

  • स्पेशल कंटेनर की सुविधा लॉन्च होने से पहले, Compose और गेम इंजन को एक साथ इस्तेमाल नहीं किया जा सकता था.

  • रीयल-टाइम इंजन में ऐप्लिकेशन डेवलपमेंट की बात करें, तो हम दो अलग-अलग तरीकों से काम करते हैं.

  • नेटिव Android डेवलपर के लिए, ऐप्लिकेशन रनटाइम को Jetpack Compose के हिसाब से बनाया गया है.

  • इसे तेज़ी से काम करने वाले और रिऐक्टिव यूज़र इंटरफ़ेस (यूआई), बैटरी की बेहतर परफ़ॉर्मेंस, डाइनैमिक लेआउट, सिस्टम की सुलभता, और टेक्स्ट इनपुट की सुविधा के लिए डिज़ाइन किया गया है.

  • जब कोई गतिविधि नहीं होती है, तब यह कुछ नहीं करता. साथ ही, इसके काम करने के तरीके का अनुमान लगाया जा सकता है.

  • गेम इंजन डेवलपर के लिए, रीयल-टाइम रेंडरिंग का डोमेन सबसे अहम है. इसमें Godot और Unity जैसे टूल का इस्तेमाल किया जाता है.

  • वे सिर्फ़ उपयोगकर्ता के इंटरैक्शन का इंतज़ार नहीं करते.

  • ये लगातार टिक और रेंडर लूप चलाते हैं.

  • इसलिए, यह 60, 90, 100 फ़्रेम रेट प्रति सेकंड हो सकता है.

  • ये फ़िज़िक्स कैलकुलेशन, कॉम्प्लेक्स शेडर, स्केलेटल ऐनिमेशन, और स्पेशल ऑडियो जैसी सुविधाएं देते हैं. साथ ही, इनकी परफ़ॉर्मेंस भी शानदार होती है.

3D इंजन इंटिग्रेट करने के तरीके

  • अगर आपको दोनों तरह के फ़ायदे चाहिए, तो क्या करना होगा?

  • पहले, डेवलपर को इनमें से कोई एक तरीका अपनाना पड़ता था.

  • पहला विकल्प यह है कि आप अपने पूरे ऐप्लिकेशन को Godot या Unity जैसे किसी असली इंजन में फिर से लिखें.

  • अपने मौजूदा कोड बेस को हटाने का मतलब है कि आपको अपने नेटिव Compose यूज़र इंटरफ़ेस (यूआई) का ज़्यादातर हिस्सा खोना पड़ेगा. साथ ही, आपको बेसिक टेक्स्ट और नेविगेशन को फिर से लागू करना होगा. इसके अलावा, आपको लिखे गए शब्दों को सुनने की सुविधा जैसे Android एपीआई के लिए रैपर लिखने होंगे.

  • दूसरा विकल्प यह है कि रॉ Vulkan और OpenXR एपीआई का इस्तेमाल करके, अपने खास ऐप्लिकेशन में शुरुआत से ही लो लेवल 3D ग्राफ़िक रेंडरिंग पाइपलाइन बनाई जाए.

  • प्रॉडक्ट की सुविधाएं देने के बजाय, आपको अपना मालिकाना हक वाला इंजन बनाना होगा.

  • इसलिए, आज मैं आपको Godot और Unity को लाइब्रेरी के तौर पर इस्तेमाल करने का तरीका बता सकता हूं.

  • स्पेशल कंटेनर की मदद से, अब आपको दोनों तरह के फ़ॉर्मैट का फ़ायदा मिल सकता है.

  • इसके लिए, Jetpack ऐप्लिकेशन की लाइब्रेरी में रीयल-टाइम इंजन को सीधे तौर पर एंबेड किया जा सकता है.

  • आपके पास प्रोडक्शन Android ऐप्लिकेशन के स्ट्रक्चर, कारोबारी नियम, और ऐप्लिकेशन में 3D स्पेशल कॉन्टेंट को आसानी से रेंडर करने की सुविधा को बनाए रखने का विकल्प होता है.

  • इसका मतलब है कि आपके पास सभी साइज़, डाइमेंशन या कॉन्फ़िगरेशन वाले डिवाइसों के लिए एक APK हो सकता है.

  • अगर आपको सिर्फ़ 3D कॉन्टेंट नहीं दिखाना है, तो आपके पास गतिविधि या फ़्रैगमेंट में लाइब्रेरी को लोड न करने और अपने कोड बेस को साफ़ रखने का विकल्प होता है. इससे आपको कई डिवाइसों पर काम करने की सुविधा मिलती है.

  • इसका तरीका आसान है. ऐप्लिकेशन को अलग करें और रीयल-टाइम इंजन को एक लाइब्रेरी बनाएं.

  • Android ऐप्लिकेशन या इंजन में से किसी एक को चुनने के बजाय, इंजन को एम्बेड की गई सॉफ़्टवेयर लाइब्रेरी के तौर पर इस्तेमाल किया जाता है. इसे एएआर डिपेंडेंसी भी कहा जाता है.

  • इसे किसी भी अन्य लाइब्रेरी की तरह, सीधे अपने Gradle बिल्ड में पुल किया जा सकता है.

  • आपका नेटिव Android कोड, 3D कॉम्पोनेंट को होस्ट करने वाले ऐप्लिकेशन का मुख्य हिस्सा बना रहता है.

  • Jetpack Compose XR, नेटिव ऐक्सेसिबल फ़्लूइड यूज़र इंटरफ़ेस (यूआई) को रेंडर करने और ऐप्लिकेशन की स्थिति को मैनेज करने का काम करता है.

  • जब किसी 3D एलिमेंट की ज़रूरत होती है, तो ऐप्लिकेशन में एम्बेड किया गया रेंडरिंग व्यू होस्ट किया जाता है. इसे गेम इंजन ही रेंडर करता है.

  • आज हम इस बारे में ज़्यादा जानकारी देंगे कि Godot के लिए यह सुविधा कैसे काम करती है.

  • सबसे पहले, हम Kotlin और GDScript के बीच एम्बेड करने और दोनों तरफ़ से कम्यूनिकेशन करने के तरीके के बारे में जानेंगे.

  • इसके बाद, हम नए OpenXR एक्सटेंशन, स्पेशल कंटेनर के बारे में जानेंगे. इससे मल्टीटास्किंग और स्पेशल लेआउट की नई जनरेशन को अनलॉक किया जा सकेगा.

  • आइए, उस डेमो पर एक नज़र डालते हैं जो मैंने पहले दिखाया था.

  • मैंने इसे Godot और Android Studio का इस्तेमाल करके बनाया है.

  • इस क्लिप में, Jetpack Compose for XR का इस्तेमाल करके रेंडर किए गए नेटिव 2D यूज़र इंटरफ़ेस (यूआई) के स्पेशल पैनल दिखाए गए हैं. इनमें स्कोरबोर्ड, सूचनाएं, और हाइलाइट शामिल हैं.

  • इसके अलावा, Godot एक सॉकर मैच का सिम्युलेशन रेंडर कर रहा है.

Kotlin और Godot के बीच कम्यूनिकेशन

  • यहां Kotlin से GDScript में कम्यूनिकेट करने के तरीके का ब्रेकडाउन देखा जा सकता है.

  • इसके लिए, Godot एक प्लग-इन आर्किटेक्चर उपलब्ध कराता है.

  • आपको एक लाइटवेट Kotlin क्लास लिखनी होगी, जो GodotPlugin को एक्सटेंड करती हो.

  • यह क्लास, जेवीएम और Godot के C++ रनटाइम के बीच इंटरफ़ेस के तौर पर काम करती है.

  • इस वजह से, आपके ऐप्लिकेशन और Godot के बीच इंटरैक्शन बहुत ही आसान होता है.

  • Godot का आर्किटेक्चर, Godot और Kotlin के बीच दोतरफ़ा कम्यूनिकेशन की सुविधा देता है.

  • जब Godot में कोई गेम इवेंट होता है, जैसे कि गोल या फ़ाउल, तो Godot सीधे Kotlin फ़ंक्शन को कॉल कर सकता है और Compose यूज़र इंटरफ़ेस (यूआई) को रीयल टाइम में अपडेट कर सकता है.

  • इसके बाद, जब उपयोगकर्ता मीडिया प्लेयर के साथ इंटरैक्ट करता है, जैसे कि वीडियो को रोकना या अलग-अलग हाइलाइट चुनना, तो Kotlin, Godot को एक सिग्नल भेजता है. इसके बाद, गेम इंजन से जुड़ी कार्रवाइयां होती हैं.

  • Godot प्लग-इन को पार करने के बाद, परफ़ॉर्मेंस और मैनेजमेंट, डोमेन की लाइनों के हिसाब से काम करता है.

  • Android ऐप्लिकेशन और Godot प्रोजेक्ट के लिए, सबसे सही स्टैंडर्ड तरीके अपनाएं.

  • अब हम आर्किटेक्चर पर नज़र डालेंगे.

  • आइए, देखते हैं कि हम इसे Android ऐप्लिकेशन में Godot में कैसे जोड़ते हैं.

  • Godot इंजन रनटाइम को Maven के ज़रिए, एएआर के तौर पर उपलब्ध कराया जाता है.

  • अपने बिल्ड में इंटिग्रेट करने के लिए, Android के स्टैंडर्ड तरीकों का इस्तेमाल करें.

  • एक ज़रूरी बात है, जिसके बारे में आपको तुरंत पता होना चाहिए. इसे मैं ऐसेट ट्रैप कहता हूं.

    androidResources {
         ignoreAssetsPattern = "!.svn:!.git:!.gitignore:!.ds_store:!*.scc:<dir>_*:!CVS:!*~:!android:!export_presets.cfg"
     }
  • Godot प्रोजेक्ट में, आपके सीन और प्रोजेक्ट की कॉन्फ़िगरेशन फ़ाइलें एक साथ मौजूद होती हैं.

  • Godot को एम्बेड करने पर, Godot प्रोजेक्ट को सीधे आपके ऐसेट फ़ोल्डर में जोड़ दिया जाता है.

  • इसका मतलब है कि इंपोर्ट किए गए सभी 3D मेश, कंप्रेस किए गए टेक्सचर, और कंपाइल किए गए टेक्सचर, रनटाइम आर्टफ़ैक्ट के तौर पर .godot नाम की छिपी हुई फ़ाइल में सेव होते हैं.

  • डिफ़ॉल्ट रूप से, Android बिल्ड करते समय इस फ़ोल्डर को हटा देगा. इसलिए, यह डॉट पीरियड वाली किसी भी डायरेक्ट्री को अनदेखा कर देता है.

  • इसलिए, अगर आपने अपने ऐप्लिकेशन को सामान्य तरीके से सिंक और बिल्ड किया, तो APK बन जाएगा. हालांकि, वह क्रैश हो जाएगा और लोड नहीं होगा, क्योंकि वे चीज़ें APK में कंपाइल नहीं की गई हैं.

  • इस समस्या को ठीक करने के लिए, build.gradle में AAPT के विकल्पों को साफ़ तौर पर बदला जा सकता है. साथ ही, Godot से शुरुआती बिंदुओं को अनुमति दी जा सकती है.

  • ऐसा करने के बाद, Godot एडिटर में Godot सीन को एक बार चलाया जा सकता है. इसके बाद, Android Studio से बाकी सब कुछ बनाया जा सकता है.

  • स्पेशल कंटेनर और नए OpenXR का इस्तेमाल करने के लिए, हमारा सुझाव है कि आप Godot 4.8 और इसके बाद के वर्शन, Godot वेंडर एक्सटेंशन 5.2, और Khronos OpenXR लोडर 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")

Jetpack Compose में Godot को रेंडर करना

  • ऐसेट पैकेज होने के बाद, Godot और Compose असल में कैसे रेंडर होते हैं?

  • Godot लाइब्रेरी, 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 }
        )
    }
  • यह एक SurfaceView दिखाता है, जिसे Godot रेंडर करता है.

  • इसलिए, इसे ऐप्लिकेशन बनाने की सुविधा में रैप किया जाता है. अब Godot को इकाई पैनल में कहीं भी साइज़, पैड, और प्लेस किया जा सकता है.

    Subspace {
            SpatialPanel(
                modifier = SubspaceModifier
                    .width(960.dp)
                    .height(540.dp)
                    .offset(z = (-200).dp)
                    .movable()
    ,
        ) {
            GodotView(
                godotHost = godotHost,
                modifier = Modifier.fillMaxSize(),
            )
        }
    }
  • इंजन, लाइफ़ साइकल को खुद मैनेज नहीं कर सकता.

    @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)
           }
       }
    
    }
  • इसलिए, आपकी गतिविधि को GodotHost इंटरफ़ेस लागू करना होगा.

  • इससे यह पक्का होगा कि आपकी गतिविधि में होने वाली कोई भी लाइफ़ साइकल, चाहे वह रोकी गई हो या फिर से शुरू की गई हो, Godot इंजन को भेजी जाएगी. साथ ही, अब यह लाइफ़ साइकल के बारे में जानकारी देने वाला है.

  • इसके बाद, हमें अपने खास ऐप्लिकेशन और इंजन को जोड़ना होगा, ताकि वे आसानी से कम्यूनिकेट कर सकें.

  • इसलिए, ऐसा करने के लिए हम GodotHost इंटरफ़ेस का इस्तेमाल कर सकते हैं.

    class MainActivity : ComponentActivity(), GodotHost {
        override fun getHostPlugins(engine: Godot): Set<GodotPlugin> =
            setOf(SoccerGodotPlugin(engine))
    }
  • हम Godot प्लग-इन को तय कर सकते हैं. साथ ही, यहां यह पक्का किया जा सकता है कि Godot के चालू होने पर, प्लग-इन को Compose की ओर से रजिस्टर किया गया हो.

  • प्लग इन रजिस्टर हो गया है. अब Android को सेट अप करें, ताकि वह Godot को सिग्नल भेजकर कम्यूनिकेट कर सके.

  • हम अपने Kotlin कोड में, GodotPlugin का एक इंस्टेंस बनाएंगे.

  • हम अपनी गतिविधि में, प्लगिन को Godot लाइब्रेरी के साथ रजिस्टर करेंगे.

  • इसके बाद, Kotlin में हम GodotPlugin बना रहे हैं, उसका नाम रजिस्टर कर रहे हैं, और Godot को उन सभी सिग्नल के बारे में बता रहे हैं जिनका इस्तेमाल हमें इस पैकेज में करना है.

    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),
        )
    
    }
  • इसके बाद, हमारे पास एक Godot स्क्रिप्ट है, जहां आपको इस प्लग इन का इंस्टेंस मिल सकता है.

    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)
  • हम देख सकते हैं कि क्या रजिस्टर किया गया है. इसके बाद, हम फ़ंक्शन को उन सिग्नल से जोड़ सकते हैं जिन्हें हमने रजिस्टर किया है.

  • इसलिए, emitSignal जैसे किसी फ़ंक्शन को कॉल करने के बाद, इसे Kotlin कोड से पास किया जा सकता है. इसके बाद, यह सीधे Godot पर चला जाता है.

    class SoccerGodotPlugin(godot: Godot) : GodotPlugin(godot) {
    
        fun sendCommandToGodot(command: String) {
            emitSignal("match_command", command)
        }
    }
  • Godot को Kotlin के साथ कम्यूनिकेट करने के लिए, मान लें कि कोई गोल होता है, तो हम प्लगिन इंस्टेंस का इस्तेमाल करके, Kotlin फ़ंक्शन को सीधे Godot स्क्रिप्ट से कॉल कर सकते हैं.

    func _on_goal_scored(minute: int, home: int, away: int) -> void:
    
        if soccer_plugin:
            soccer_plugin.onMatchEvent("GOAL", minute, home, away)
  • इसलिए, सबसे पहले Kotlin फ़ंक्शन को 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)
            }
        }
  • इंजन, जेएनआई मार्शलिंग को बैकग्राउंड में मैनेज करता है.

  • इसके बाद, हम Godot Script से सीधे फ़ंक्शन को कॉल कर सकते हैं.

  • इसका इस्तेमाल करते समय एक बात का ध्यान रखें कि UsedByGodot के लिए, स्ट्रिंग का पूरी तरह से मैच होना ज़रूरी है.

  • इसलिए, अगर आपकी Godot स्क्रिप्ट onMatchEvent को कॉल करती है, तो Kotlin में भी इसका नाम यही होना चाहिए. इससे, कैमल केस या स्नेक केस में अपने-आप कन्वर्ज़न नहीं होगा.

  • इसलिए, आपको बस यह पक्का करना होगा कि दोनों तरीके बिलकुल एक जैसे हों.

  • प्लग इन कनेक्ट होने के बाद, इस बातचीत का सबसे अहम हिस्सा वह है जिसे मैं मेंटल मॉडल कहता हूं.

  • गेम इंजन और Jetpack Compose, दो अलग-अलग आर्किटेक्चर पर काम करते हैं.

  • गेम इंजन, लगातार फ़्रेम टिक पर काम करता है.

  • चाहे आपने कुछ बदला हो या नहीं, गेम इंजन की टिक प्रोसेस हर सेकंड 60, 90, 100 बार होती है. इससे रीयल-टाइम में रेंडरिंग बेहतर और आसान हो जाती है.

  • Godot जैसे इंजन, जीपीयू और सीपीयू साइकल का बेहतर तरीके से इस्तेमाल करते हैं.

  • वहीं दूसरी ओर, Jetpack Compose चीज़ों को रिएक्टिव तरीके से हैंडल करता है और यह स्टेट-ड्रिवन होता है.

  • जब किसी ऐप्लिकेशन में कोई बदलाव नहीं होता है, तो Compose काम करता रहता है और सीपीयू का इस्तेमाल नहीं करता.

  • अगर आपको बिना किसी रणनीति के दो रनटाइम चलाने के लिए इन्हें कनेक्ट करना है, तो ये फ़िलॉसफ़ी आपस में टकरा सकती हैं.

  • तो चलिए, देखते हैं कि हम इस समस्या को कैसे हल कर सकते हैं. इसके लिए, मैं टिक लीक का इस्तेमाल करता हूं.

  • यहां हमें 3D अनुभव मिलता है. जैसे, स्कोरबोर्ड टाइमर.

  • आपकी इंजन स्क्रिप्ट में, एक ti- टाइमर वैरिएबल होता है जो लगातार बदलता रहता है.

  • अगर इस वैरिएबल को Compose में बिना सोचे-समझे फ़ॉरवर्ड किया जाता है, तो मान लें कि हर सेकंड 90 बार, आपको हर सेकंड 90 बार इस रीकंपोज़िशन लूप में जाना होगा.

  • इसका मतलब है कि Compose, हर सेकंड 90 बार आपके कंपोज़ेबल का फिर से आकलन करेगा, लेआउट को फिर से मेज़र करेगा, और यूज़र इंटरफ़ेस (यूआई) ट्री को फिर से ड्रॉ करेगा.

  • आपको फ़्रेम ड्रॉप और यूज़र इंटरफ़ेस (यूआई) में लैग देखने को मिलेगा. साथ ही, फ़ोन बहुत जल्दी गर्म हो जाएगा.

  • इसलिए, सबसे ज़रूरी नियम यह है कि लगातार चलने वाले इंजन लूप को कभी भी रिएक्टिव यूज़र इंटरफ़ेस (यूआई) लूप को कंट्रोल न करने दें.

  • इस उदाहरण में, Godot को स्कोरबोर्ड टाइमर को लगातार अपडेट करने के बजाय, मैंने यह पक्का किया है कि Android ज़रूरत पड़ने पर हर सेकंड में एक बार इस डेटा को फ़ेच करे.

    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, ...) }
    }
  • आखिर में, थ्रेड की सीमा को हमेशा ध्यान में रखें.

  • Godot गेम इंजन लूप, अपने रेंडर थ्रेड पर लगातार चलता रहता है.

  • यूज़र इंटरफ़ेस (यूआई) और Android यूज़र इंटरफ़ेस (यूआई) को Android के मुख्य थ्रेड पर लागू करना ज़रूरी है.

  • इसलिए, अपने व्यू मॉडल की स्थितियों को अपडेट करने से पहले, हमेशा यह पक्का करें कि आपका Kotlin प्लग इन, Dispatchers.Main या सुरक्षित स्थिति वाले प्रिमिटिव का इस्तेमाल करके इवेंट डिस्पैच करता हो.

    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
           )
       }
  • इंजन में बार-बार होने वाले काम और Compose में अलग-अलग सिमैंटिक को बनाए रखकर, आपको बिना किसी रुकावट के 3D व्यूपोर्ट मिल सकते हैं. इसके लिए, आपको अपने नेटिव ऐप्लिकेशन की बैटरी लाइफ़, सुलभता या परफ़ॉर्मेंस से समझौता नहीं करना पड़ेगा.

  • अब देखते हैं कि Godot प्रोजेक्ट में स्पेशल कंटेनर कैसे जोड़े जा सकते हैं.

  • सबसे पहले, ऐप्लिकेशन को फ़ुल-स्पेस में सेट अप करें. इसके बाद, स्पेशल कंटेनर को उसकी बाउंडिंग हिंट के साथ चालू करें. इसके बाद, Android मेनिफ़ेस्ट फ़ाइल में XR लोडर लाइब्रेरी सेट करें.

    <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 में एक्सआर प्रोजेक्ट की अपनी सेटिंग होती हैं. इनमें, OpenXR, एक्सआर शेडर, और स्पेशल कंटेनर को चालू किया जा सकता है. इसके लिए, मुझे सिर्फ़ इसके एडिटर का इस्तेमाल करना होगा.

  • Godot में स्पेशल कंटेनर चालू करने के लिए, आपको बस इतना ही करना होगा.

  • अब आपने Godot को Android XR, स्पेशल कंटेनर, और इन सभी सुविधाओं के साथ काम करने के लिए सेट अप कर लिया है. स्पेशल कंटेनर, स्पेशल पैनल, और जेएक्सआर का इस्तेमाल करने के सबसे सही तरीके जानने के लिए, हमारा सुझाव है कि आप डेवलपर वेबसाइट पर जाएं.

  • हमने अभी एक उदाहरण देखा कि Godot Engine और Android XR में स्पेशल कंटेनर का इस्तेमाल कैसे किया जाता है. मुझे यकीन है कि आप में से कई लोग, ऐसे डेवलपर से सुनना चाहेंगे जो Android XR के अनुभव के लिए, स्पेशल कंटेनर पर लगातार काम कर रहा है.

  • अब मैं NBA के जोश और रॉड्रिगो को बुलाना चाहूंगा. ये उन शुरुआती पार्टनरों में से एक हैं जो अपने ऐप्लिकेशन में स्पेशल कंटेनर का इस्तेमाल कर रहे हैं.

NBA कोर्ट व्यू के बारे में खास जानकारी

  • धन्यवाद, ल्यूक.

  • नमस्ते दोस्तो.

  • मेरा नाम जॉश है और मैं NBA में Android डेवलपर हूं.

  • आज मेरे साथ मेरे कलीग रॉड्रिगो सांचेज़ हैं. वे Beyond Sports में Unity सॉफ़्टवेयर डेवलपर हैं.

  • आज हम आपको यह दिखाना चाहते हैं कि हमने NBA ऐप्लिकेशन के लिए कोर्ट व्यू बनाने के लिए, Unity को लाइब्रेरी के तौर पर कैसे इस्तेमाल किया.

  • NBA में, हम हमेशा अपने प्रशंसकों को नए अनुभव देने की कोशिश करते हैं.

  • इसलिए, Google और Beyond Sports के साथ मिलकर, हम कोर्ट व्यू की सुविधा पेश कर रहे हैं.

  • कोर्ट व्यू में, NBA गेम को 3D में फिर से बनाया जाता है. इससे League Pass के सदस्यों को गेम को नए ऐंगल से देखने का मौका मिलता है. ऐसा सामान्य ब्रॉडकास्ट वीडियो में नहीं किया जा सकता.

  • कोर्ट को स्केल करें और फिर से घुमाएं. इसके बाद, उसे अपने स्पेस में अपनी पसंद की जगह पर रखें.

  • इसके चारों ओर घूमें और गेम को अलग-अलग ऐंगल से देखें.

  • इस दौरान, NBA ऐप्लिकेशन को सामान्य तरीके से इस्तेमाल किया जा सकता है. जैसे, सिंक किए गए आंकड़े देखना या लाइव ब्रॉडकास्ट देखना.

  • यह सब कैसे सेट अप किया जाता है?

  • कोर्ट व्यू को Unity में रेंडर किया जाता है. साथ ही, हमारा Android ऐप्लिकेशन, Unity को लाइब्रेरी के तौर पर इस्तेमाल करके इसे दिखाता है.

  • इसके बाद, हम एक Android संग्रह बनाते हैं, जिसे Maven Central पर होस्ट किया जाता है. इसे NBA ऐप्लिकेशन, एक डिपेंडेंसी के तौर पर शामिल कर सकता है.

  • इससे हमें Android और Unity के बीच काम को बांटने में मदद मिली.

  • इससे हम सभी अपनी-अपनी विशेषज्ञता के हिसाब से काम कर सकते हैं. साथ ही, एक साथ काम करके डेवलपमेंट की प्रोसेस को तेज़ कर सकते हैं.

  • उदाहरण के लिए, जब हमारी टीम Compose में फ़ुल-स्पेस अनुभव पर काम कर रही थी, तब Unity की टीम 3D कोर्ट की स्केलिंग और लेआउट पर फ़ोकस कर सकती थी.

  • अब मैं रॉड्रिगो को Unity प्रोजेक्ट के बारे में ज़्यादा जानकारी देने के लिए आमंत्रित करता हूं. वे बताएंगे कि उन्होंने Unity को लाइब्रेरी के तौर पर कैसे इस्तेमाल किया.

Unity को लाइब्रेरी के तौर पर इंटिग्रेट करना

  • धन्यवाद, जोश.

  • Beyond Sports में हम कुछ सालों से एनबीए के साथ मिलकर काम कर रहे हैं. हम मैचों के असल डेटा का इस्तेमाल करके, 3D विज़ुअलाइज़ेशन दिखाते हैं.

  • इस प्रोजेक्ट के तहत, हमने अपने 3D सिम्युलेशन को Android XR के साथ काम करने लायक बनाया.

  • हमारा सिम्युलेशन, C# के साथ Unity इंजन पर बनाया गया है.

  • हम Unity as a Library का इस्तेमाल करके, सिम्युलेशन को Android में एक्सपोर्ट करते हैं. साथ ही, Kotlin का इस्तेमाल करके इसके बगल में एक कम्यूनिकेशन लेयर बनाते हैं.

  • इन दोनों मॉड्यूल को एक साथ बंडल करने से पहले, NBA को अपने ऐप्लिकेशन में शामिल करने के लिए, एक Maven डिपेंडेंसी के तौर पर बंडल करना होगा.

  • स्क्रीन पर, आपको उसी सिम्युलेशन का एक छोटा वीडियो दिख रहा है जो जोश ने पहले दिखाया था. यह वीडियो, एडिटर पर चल रहा है.

  • इसके लिए, हम Unity के लिए Android XR Extensions पैकेज का इस्तेमाल करते हैं.

  • आइए, हम आपको इस प्रोसेस के बारे में बताते हैं.

  • इसलिए, Unity में गेम बनाने से पहले, पक्का करें कि आपने सही सेटिंग और प्लग-इन चुने हों.

  • Unity का XR प्लग-इन मैनेजमेंट पैकेज और Unity के लिए Google का Android XR Extensions पैकेज डाउनलोड करें.

  • OpenXR Android XR Extensions प्रोवाइडर का इस्तेमाल करने के लिए, उन्हें XR प्लग-इन मैनेजमेंट की सेटिंग में कॉन्फ़िगर करें.

  • हमें अपने इस्तेमाल के उदाहरण के लिए, Spatial Containers का इस्तेमाल करना था. इसलिए, हमने उस सुविधा को चालू किया और सेशन मैनेजमेंट की सेटिंग में जाकर, इमर्सिव एक्सआर को बंद कर दिया.

  • हमने किसी भी XR इनपुट पैकेज का इस्तेमाल नहीं किया, क्योंकि सभी इनपुट को Unity के बाहर, नेटिव तौर पर मैनेज किया जाता है.

  • इसके बाद, प्लेयर की सेटिंग में जाकर, अपने प्रोजेक्ट को कम से कम 24 या इससे ज़्यादा एपीआई लेवल का इस्तेमाल करने के लिए सेट करें. साथ ही, ऐप्लिकेशन के एंट्री पॉइंट को नई गेम गतिविधि पर सेट करें.

  • ध्यान रखें कि गतिविधि को अनचेक ही रहने दें.

  • हम सिर्फ़ ARM64 के लिए ऐप्लिकेशन बनाना चाहते हैं. इसलिए, हम उस टारगेट आर्किटेक्चर को चुनते हैं. साथ ही, रिज़ॉल्यूशन सेक्शन में, हम 'बैकग्राउंड में चलाएं' को चालू करते हैं, ताकि आप अन्य ऐप्लिकेशन के साथ मल्टीटास्किंग कर सकें और 3D सिम्युलेशन को साइड में चला सकें.

  • हम साइज़ बदलने की सुविधा भी चालू करते हैं, क्योंकि सिस्टम की अनुमति के अनुरोध जैसे डायलॉग रेंडर करने के लिए यह ज़रूरी है.

  • आखिर में, हमारा सुझाव है कि Android XR ऐप्लिकेशन को रेंडर करने के लिए, Universal Render Pipeline और Vulkan Graphics API का इस्तेमाल करें.

  • आपकी यूआरपी ऐसेट में, एचडीआर और पोस्ट-प्रोसेसिंग की सुविधा बंद होनी चाहिए.

  • इन सभी चरणों को पूरा करने के बाद, बिल्ड बनाया जा सकता है.

  • बिल्ड विंडो पर जाएं. अगर Unity 6.1 या इसके बाद के वर्शन का इस्तेमाल किया जा रहा है, तो Android XR बिल्ड प्रोफ़ाइल को चुना जा सकता है. इससे, Android मोबाइल के लिए भी बिल्ड बनाने पर, सेटिंग को अलग-अलग किया जा सकता है.

  • प्रोजेक्ट बनाने से पहले, पक्का करें कि आपने प्रोजेक्ट एक्सपोर्ट करने की सुविधा चालू की हो.

  • Unity के बिल्ड होने के बाद, आपके पास एक ऐसा Android प्रोजेक्ट होगा जो काम कर रहा होगा. इसे Android Studio में असेंबल किया जा सकता है और किसी भी अन्य Unity APK की तरह डिवाइस पर चलाया जा सकता है.

  • Android Studio में, एक्सपोर्ट किए गए बिल्ड को लाइब्रेरी में बदलें, ताकि उसे किसी दूसरे ऐप्लिकेशन में इस्तेमाल किया जा सके.

    // 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")
    }
    }
  • हमारे इस प्रोजेक्ट में चार अलग-अलग मॉड्यूल हैं: मुख्य APK मॉड्यूल, जिसे एनबीए के रैपिंग ऐप्लिकेशन के लिए मॉक-अप प्लेसहोल्डर के तौर पर टेस्ट किया जाएगा. Unity लाइब्रेरी मॉड्यूल, जिसमें एक्सपोर्ट किया गया बिल्ड मौजूद है. nativeCommunications मॉड्यूल, जिसमें हमारी कम्यूनिकेशन लेयर मौजूद है. इसे हमने गतिविधियों के बीच बनाया है. इसके अलावा, packageExporter मॉड्यूल, जिसका इस्तेमाल हम अपने एएआर को एक पैकेज में मर्ज करने के लिए करते हैं, ताकि उसे Maven पर डिलीवर किया जा सके.

  • कई एएआर को मर्ज करने के लिए, Android फ़्यूज़्ड लाइब्रेरी या Fat AAR जैसे पुराने कम्यूनिटी तरीकों का इस्तेमाल किया जा सकता है.

  • Unity और मुख्य गतिविधि को एक ही प्रोसेस में चलाकर, स्ट्रक्चर और कम्यूनिकेशन को आसान रखा जा सकता है.

  • हमने ऐसा नहीं किया और Unity की गतिविधि को एक अलग प्रोसेस में रखा. इससे यह फ़ायदा हुआ कि अगर Unity वाला हिस्सा क्रैश होता है, तो NBA ऐप्लिकेशन पर कोई असर नहीं पड़ता. साथ ही, वह क्रैश हुए बिना Unity को सुरक्षित तरीके से बंद कर सकता है.

  • इस फ़ैसले की वजह से, हमें इंटर-प्रोसेस कम्यूनिकेशन सिस्टम को और बेहतर बनाना पड़ा.

  • गतिविधियां अलग-अलग प्रोसेस में होती हैं. इसलिए, उनके बीच कोई मेमोरी शेयर नहीं की जाती. साथ ही, Android के कई सामान्य आईपीसी तरीके काम नहीं करते या ज़रूरी शर्तें पूरी नहीं करते.

  • हम NBA की मुख्य गतिविधि और एम्बेड की गई Unity गतिविधि के बीच कम्यूनिकेशन के लिए, Android इंटरफ़ेस डेफ़िनिशन लैंग्वेज या एआईडीएल का इस्तेमाल करते हैं.

  • इसके बाद, Android और Unity के बीच कम्यूनिकेशन भी इंटरफ़ेस पर आधारित होता है. इसके लिए, Unity में पहले से मौजूद AndroidJavaProxy का इस्तेमाल करके, Kotlin इंटरफ़ेस को C# इंटरफ़ेस के साथ रजिस्टर किया जाता है.

  • हमारा कम्यूनिकेशन मॉड्यूल मुख्य रूप से उस पर फ़ोकस करता है जिसे हम ऐक्टिविटी ब्रिज कहते हैं.

    // 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) }
    }
    }
  • दोनों गतिविधियों के पास, गतिविधि ब्रिज और उसमें मौजूद यूटिलिटी मेथड का ऐक्सेस होता है.

  • ब्रिज में उस सेवा का रेफ़रंस होता है जो हमारे तय किए गए एआईडीएल इंटरफ़ेस को लागू करती है. साथ ही, यह गतिविधियों को प्रोसेस से, माफ़ करें, सेवा से बाइंड करने का आसान तरीका उपलब्ध कराता है. इसके अलावा, यह सेवा की लाइफ़साइकल को अंदरूनी तौर पर मैनेज करता है.

  • सिम्युलेशन शुरू करने के लिए, कॉलबैक का इस्तेमाल किया जा सकता है. इसमें हम मैच आईडी को पैरामीटर के तौर पर पास करते हैं.

  • इसके बाद, ब्रिज इसे एआईडीएल के ज़रिए Unity ऐप्लिकेशन को भेजता है. यहां C# को एक निर्देश मिलता है और वह सीन को कॉन्फ़िगर करना शुरू कर देता है.

    public class NativeInterfaceListener : AndroidJavaProxy {
    [UsedImplicitly]
    public void startMatch(string matchID) {
       UnityThreadHelper.InvokeOnMain(() => _internalAPI.StartMatch(matchID));
    }
    }
  • हमें Unity के चालू होने और हमारे दिए गए निर्देशों को प्रोसेस करने के लिए तैयार होने का इंतज़ार करना होगा.

  • ऐप्लिकेशन को रैप करने वाली Unity गतिविधि, Unity के चालू होने से पहले ही चालू हो जाएगी.

  • अगर कोई निर्देश बहुत जल्दी भेजा जाता है, तो हम उसे कॉलबैक सूची में सेव कर लेते हैं.

    fun onStartMatchClickedInternal() {
       if (isServiceBound) {
           AIDLBridgeService.startMatch("match_id")
       } else {
           delayedCallbacksQueue.add(action)
       }
    }
  • Unity के बूट लोड होने और शुरू होने के बाद, जब हमें लगता है कि अब सब कुछ सुरक्षित है, तब हम C# से कम्यूनिकेट करते हैं कि Unity तैयार है.

    // After bootload and initialization
    
    using AndroidJavaClass nativeBridge = new("com.company.UnityActivity");
    using AndroidJavaObject instance = nativeBridge.GetStatic<AndroidJavaObject>("instance");
    instance.Call("onUnityReady");
  • इसके बाद, हम अपनी सूची को लूप करते हैं और सेव किए गए सभी निर्देश भेजते हैं.

    @Suppress("unused") //  Unity implicit call
    fun onUnityReady() {
       bridge.activityBridgeServiceInternal?.onUnityReady()
    }
    
    delayedCallbacksQueue.forEach { it(activityBridgeService) }
  • इस डायग्राम से बेहतर तरीके से पता चलता है कि इंटर-प्रोसेस कम्यूनिकेशन कैसे काम करता है.

  • जैसा कि आपने देखा, गतिविधियों के बीच ब्रिज का इस्तेमाल करके, हम उन कॉलबैक को मैनेज कर सकते हैं जिनके बारे में हमने बताया था.

  • हमने StateFlows सेट अप किए हैं, जिन्हें Unity से अपडेट किया जाता है. इससे पुष्टि की स्थिति या मैच के मौजूदा स्टेटस के टाइमस्टैंप जैसी वैल्यू को मॉनिटर किया जा सकता है.

  • इसके अलावा, हम क्रेडेंशियल को एन्क्रिप्ट (सुरक्षित) करने की प्रोसेस को भी मैनेज करते हैं. साथ ही, हम Unity से मिलने वाली किसी भी गड़बड़ी को NBA को भेजते हैं, ताकि वह गड़बड़ी को आसानी से ठीक कर सके. साथ ही, यह तय कर सके कि ऐप्लिकेशन लेवल पर उन्हें सबसे सही तरीके से कैसे मैनेज किया जाए.

  • बस इतना ही.

  • अब मैं जोश को स्टेज पर वापस बुलाना चाहूंगा, ताकि वे इस बारे में ज़्यादा बता सकें कि कोर्ट व्यू यूनिटी पैकेज को NBA ऐप्लिकेशन में कैसे इंटिग्रेट किया जाता है.

  • हम इन Gradle डिपेंडेंसी का इस्तेमाल करके, Unity को अपने Jetpack Compose ऐप्लिकेशन के साथ लाइब्रेरी के तौर पर इंटिग्रेट करते हैं: 3D सेशन को ऐक्सेस करने के लिए XR रनटाइम, हमारी Unity गतिविधि को होस्ट करने और स्पेशल एनवायरमेंट को टॉगल करने के लिए Jetpack SceneCore, और हमारे Compose कोड में स्पेशल पैनल को ऐक्सेस करने के लिए Jetpack Compose for XR.

    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")
    }
  • इसके बाद, आइए देखते हैं कि 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)
            }
        }
    }
  • कोर्ट व्यू लॉन्च करने से पहले, हम आपको फ़ुल स्पेस में ले जाते हैं.

  • Unity को होल्ड करने वाले स्पेशल पैनल को दिखाने के लिए, ऐसा करना ज़रूरी है.

  • इसके बाद, हम एआईडीएल पर गेम की जानकारी भेजते हैं, ताकि किसी गेम के लिए कोर्ट व्यू लॉन्च किया जा सके.

  • इसके बाद, Unity अपने-आप शुरू हो जाता है.

  • हमारा Android संग्रह, इसे Unity गतिविधि के लिए इंटेंट के तौर पर भेजता है. साथ ही, हम इसे SpatialActivityPanelController में रैप करते हैं.

  • इसके बाद, हम उस कंट्रोलर को SpatialActivityPanel को पास कर देते हैं.

  • इस पैनल में कोर्ट व्यू होता है. इसे हमारे मुख्य स्पेशल पैनल के साथ, हमारे सबस्पेस में शेयर किया जाता है.

  • इससे हम दोनों को एक साथ बिना किसी समस्या के कंट्रोल कर सकते हैं.

  • बस इतना ही!

  • मैं इसे ल्यूक को वापस भेज दूंगा.

नतीजा और संसाधन

  • बहुत-बहुत धन्यवाद.

  • जैसा कि आपने देखा, एनबीए ने Unity और Jetpack Compose के डेवलपमेंट को अलग-अलग करके, कोर्ट व्यू को तेज़ी से डेवलप किया.

  • स्पैशियल कंटेनर का इस्तेमाल शुरू करने और अपने Android XR ऐप्लिकेशन में इसका इस्तेमाल करने के बारे में ज़्यादा जानने के लिए, developer.android.com/develop/xr पर डेवलपर दस्तावेज़ देखें.

  • आज हमारे साथ जुड़ने के लिए बहुत-बहुत धन्यवाद. हमें यह देखने का बेसब्री से इंतज़ार है कि आप क्या बनाते हैं.

कोड

कोड स्निपेट

"Kotlin और Godot के बीच कम्यूनिकेशन" में 09:30 पर Kotlin स्क्रिप्ट स्निपेट
androidResources {
     ignoreAssetsPattern = "!.svn:!.git:!.gitignore:!.ds_store:!*.scc:<dir>_*:!CVS:!*~:!android:!export_presets.cfg"
 }
"Kotlin और Godot के बीच कम्यूनिकेशन" में 10:27 पर Kotlin स्क्रिप्ट स्निपेट
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 में Godot को रेंडर करना" में 10:48 पर Kotlin स्निपेट
@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 }
    )
}
"Jetpack Compose में Godot को रेंडर करना" में 10:55 पर Kotlin स्निपेट
Subspace {
        SpatialPanel(
            modifier = SubspaceModifier
                .width(960.dp)
                .height(540.dp)
                .offset(z = (-200).dp)
                .movable()
,
    ) {
        GodotView(
            godotHost = godotHost,
            modifier = Modifier.fillMaxSize(),
        )
    }
}
"Jetpack Compose में Godot को रेंडर करना" में 11:05 पर Kotlin स्निपेट
@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)
       }
   }

}
"Jetpack Compose में Godot को रेंडर करना" में 11:29 पर Kotlin स्निपेट
class MainActivity : ComponentActivity(), GodotHost {
    override fun getHostPlugins(engine: Godot): Set<GodotPlugin> =
        setOf(SoccerGodotPlugin(engine))
}
"Rendering Godot in Jetpack Compose" में 11:52 पर Kotlin स्निपेट
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" में 12:01 पर टेक्स्ट स्निपेट
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" में 12:10 पर Kotlin स्निपेट
class SoccerGodotPlugin(godot: Godot) : GodotPlugin(godot) {

    fun sendCommandToGodot(command: String) {
        emitSignal("match_command", command)
    }
}
"Jetpack Compose में Godot को रेंडर करना" में 12:20 पर टेक्स्ट स्निपेट
func _on_goal_scored(minute: int, home: int, away: int) -> void:

    if soccer_plugin:
        soccer_plugin.onMatchEvent("GOAL", minute, home, away)
"Jetpack Compose में Godot को रेंडर करना" वीडियो में 12:30 पर Kotlin स्निपेट
@UsedByGodot
fun onMatchEvent(type: String, minute: Int, home: Int, away: Int, title: String) {

        _uiState.update { state ->
            state.copy(homeScore = home, awayScore = away, matchSeconds = minute)
        }
    }
"Jetpack Compose में Godot को रेंडर करना" में 14:35 पर Kotlin स्निपेट
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" में 14:57 पर Kotlin स्निपेट
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" में 15:22 पर एक्सएमएल स्निपेट
<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="" />
"Unity को लाइब्रेरी के तौर पर इंटिग्रेट करना" में 20:41 पर Kotlin स्क्रिप्ट स्निपेट
// 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" में 22:11 पर Kotlin स्निपेट
// 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" में 22:36 पर C# स्निपेट
public class NativeInterfaceListener : AndroidJavaProxy {
[UsedImplicitly]
public void startMatch(string matchID) {
   UnityThreadHelper.InvokeOnMain(() => _internalAPI.StartMatch(matchID));
}
}
"Integrating Unity as a library" में 22:54 पर Kotlin स्निपेट
fun onStartMatchClickedInternal() {
   if (isServiceBound) {
       AIDLBridgeService.startMatch("match_id")
   } else {
       delayedCallbacksQueue.add(action)
   }
}
"Unity को लाइब्रेरी के तौर पर इंटिग्रेट करना" में 22:57 पर C# स्निपेट
// 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" में 23:06 पर Kotlin स्निपेट
@Suppress("unused") //  Unity implicit call
fun onUnityReady() {
   bridge.activityBridgeServiceInternal?.onUnityReady()
}

delayedCallbacksQueue.forEach { it(activityBridgeService) }
"Integrating Unity as a library" में 23:50 पर Kotlin स्क्रिप्ट स्निपेट
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" में 24:10 पर Kotlin स्निपेट
// 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)
        }
    }
}