Android স্টাফ ইঞ্জিনিয়ার
মোবাইল ও Android XR-এর জন্য একটি সিঙ্গেল কোডবেসে Jetpack Compose-এর সাথে 3D গেম ইঞ্জিন (Godot ও Unity) কীভাবে ইন্টিগ্রেট করবেন তা জানুন। আমরা স্পেশাল কন্টেনারে 3D সিন এম্বেড করা, Kotlin-টু-ইঞ্জিন কমিউনিকেশন সেট-আপ করা এক্সপ্লোর করব —এতে NBA কীভাবে Android XR-এ তাদের লাইভ বাস্কেটবল অভিজ্ঞতা তৈরি করেছে তার একটি এক্সক্লুসিভ লুক দেখানো হবে।
GodotPlugin সাবক্লাস Kotlin-এর সাথে দ্বিমুখী যোগাযোগ ম্যানেজ করে।ট্রান্সস্ক্রিপ্ট করা
হ্যালো, সবাইকে স্বাগত।
আমার নাম লুক হপকিন্স।
আমি Android XR টিমের ডেভেলপার রিলেশনস ইঞ্জিনিয়ার।
আজকের আলোচনায়, আমরা কথা বলব কীভাবে আপনি Godot এবং Unity-এর মতো গেম ইঞ্জিন আপনার Android প্রোজেক্টে এম্বেড করতে পারবেন।
পরে, NBA এবং Beyond Sports থেকে জোশ এবং রড্রিগো আমার সাথে যোগ দেবেন, যারা আলোচনা করবেন যে কীভাবে তারা তাদের NBA অ্যাপ্লিকেশনে লাইব্রেরি হিসেবে Unity প্রয়োগ করছেন।
এই সেশনে, আমরা আপনার Jetpack Compose অ্যাপ্লিকেশনে Godot কীভাবে ইন্টিগ্রেট করবেন তা নিয়ে আলোচনা করব এবং তারপর জশ ও রড্রিগো তাদের NBA অ্যাপ্লিকেশন নিয়ে আলোচনা করবেন।
Android XR হল Android ইকোসিস্টেমের একটি এক্সটেনশন, যা আপনার পরিচিত এবং পছন্দের অভিজ্ঞতাগুলিকে একটি নতুন মাত্রায় নিয়ে আসার জন্য বিভিন্ন ধরনের ফর্ম ফ্যাক্টরকে প্রসারিত করে।
আপনার আগে থেকে থাকা XR এবং Android অ্যাপ এই নতুন স্পেসে নিয়ে আসার জন্য আমরা এটিকে ইনট্যুইটিভ করেছি এবং আপনি বর্তমানে যে Android প্রযুক্তি ব্যবহার করে অ্যাপ তৈরি করছেন, আমরা সেটিই ব্যবহার করছি।
Android XR হল একটি ইউনিফায়েড প্ল্যাটফর্ম যা বিভিন্ন ধরনের ডিভাইসের জন্য ডিজাইন করা হয়েছে।
Android XR ডিভাইস বিভিন্ন ধরনের অভিজ্ঞতা প্রদান করে।
আমাদের কাছে অডিও এবং ডিসপ্লে গ্লাসের জন্য অগমেন্টেড অভিজ্ঞতা এবং XR হেডসেট ও তারযুক্ত XR গ্লাসের জন্য ইমার্সিভ অভিজ্ঞতা রয়েছে।
আজ আমরা তারযুক্ত XR গ্লাস ও XR হেডসেটের জন্য ইমারসিভ অভিজ্ঞতা তৈরি করার বিষয়ে আলোচনা করব।
ইমারসিভ অভিজ্ঞতা তৈরি করার কাজটি আমাদের Jetpack XR SDK-এর মাধ্যমে অথবা রিয়েল-টাইম ইঞ্জিনের সাথে OpenXR ব্যবহার করে করা যেতে পারে।
আমরা Unity, Godot ও Unreal-এর সাথে কাজ করি, তাই আপনি যে টুলই ব্যবহার করুন না কেন, Android-এর জন্য ইমারসিভ অভিজ্ঞতা তৈরি করতে পারবেন।
এই আলোচনায়, আমি ব্যাখ্যা করব যে কীভাবে আপনি Jetpack-এর SceneCore API-এর সাথে Godot এবং নতুন OpenXR এক্সটেনশন ব্যবহার করে এটি করতে পারবেন।
এখানে আপনি দেখতে পাচ্ছেন যে Godot এবং Jetpack-এর স্পেশিয়াল প্যানেল একই সাথে পাশাপাশি চলছে।
Godot কীভাবে স্পেশাল কন্টেন্ট হিসেবে রেন্ডার করা হয় তা লক্ষ্য করুন, এটি স্পেশাল কন্টেনারের মধ্যে 2D Compose UI-এর পাশাপাশি 3D ভলিউম প্রদান করে।
এই পদ্ধতি ডেভেলপারদের ফিজিক্স সিমুলেশন, তাদের পছন্দের রিয়েল-টাইম ইঞ্জিন ব্যবহার করে কাস্টম শেডার চালানোর সুবিধা দেয়, পাশাপাশি আপনার অ্যাপ্লিকেশনের বাকি অংশে নেটিভ Android ডেভেলপমেন্ট প্র্যাকটিস প্রয়োগ করে।
Android XR ও XREAL AURA-এর সাহায্যে আপনিও এই ধরনের অভিজ্ঞতা তৈরি করতে পারবেন।
এই আলোচনায়, আমরা আপনাকে শেখাব কীভাবে Godot এবং Android Studio ব্যবহার করে এই অভিজ্ঞতা তৈরি করা যায় এবং তারপরে আপনি NBA এবং Beyond Sports-এ আমাদের পার্টনারদের কাছ থেকে শুনবেন যে কীভাবে তারা লাইব্রেরি হিসেবে Unity ব্যবহার করে এটি তৈরি করেছেন।
ঐতিহাসিকভাবে, OpenXR-এর মাধ্যমে XR অভিজ্ঞতা তৈরি করার অর্থ হল এমন একটি অভিজ্ঞতা তৈরি করা যা প্রতিটি পিক্সেল রেন্ডার করে।
এর অর্থ হল, কোনও ডেভেলপারকে দ্বিতীয় অ্যাক্টিভিটি দেখাতে হলে, তিনি একসাথে দুটি অ্যাক্টিভিটি দেখাতে পারবেন না।
এই সমস্যার সমাধান করতে, OpenXR ওয়ার্কিং গ্রুপ, Khronos, একাধিক ইন্ডাস্ট্রি লিডারের সাথে কোলাবরেশন করে OpenXR স্পেশিয়াল কন্টেনার এক্সটেনশন রিলিজ করেছে।
স্পেশিয়াল কন্টেনারকে একটি বাক্সে OpenXR হিসেবে ভাবুন।
আপনার ইঞ্জিনকে পুরো স্কাইবক্সের মালিকানা দেওয়ার পরিবর্তে, এটিকে সিস্টেম-ম্যানেজ করা 3D ভলিউম প্রদান করা হয় যা Jetpack-এর XR স্পেশাল প্যানেল, অন্যান্য স্পেশাল কন্টেনার এবং স্ট্যান্ডার্ড Android অ্যাক্টিভিটির সাথে পাশাপাশি থাকতে পারে।
ধরা যাক, আপনি একটি ইমারসিভ গেম চালানোর সময় একসাথে দুটি স্পেশাল প্যানেল দেখিয়ে Android XR ব্যবহার করতে চান।
আপনি ইঞ্জিনে সবকিছু রেন্ডার করেন এবং এটি এই ব্যবহারের ক্ষেত্রে একই সময়ে Godot এডিটর এবং একটি স্পেশিয়াল কন্টেনার চালানো সম্ভব করে তোলে।
Spatial API লেভেল ৪ থেকে, Google, Android XR-এ Spatial Containers প্রয়োগ করেছে।
স্পেশিয়াল কন্টেনার দুটি গুরুত্বপূর্ণ ডিসপ্লে মোড সাপোর্ট করে।
প্রথমটি হল বাউন্ডেড মোড।
এটি বেশ কিছু বিল্ট-ইন এড়িয়ে যাওয়ার সুবিধা সহ একটি নির্দিষ্ট 3D ভলিউম প্রদান করে।
একজন ব্যবহারকারী আপনার স্পেশিয়াল কন্টেনার ধরতে পারেন, তারা এটিকে ঘোরাতে পারেন, পজিশন করতে পারেন, ধরতে পারেন, মিনিমাইজ করতে পারেন এবং এটি স্টেরিও ডেপথ প্রোজেকশন সাপোর্ট করে, যা আপনাকে সেই ম্যাজিক উইন্ডো বা সেই 3D পপ-আউট এফেক্ট দেয় যা কন্টেনারের এজ ক্লিপ করার সময়ও আপনার কাছে থাকে।
দ্বিতীয়টি হল আনবাউন্ডেড মোড।
ব্যবহারকারী যদি সম্পূর্ণ ইমার্শন চান, তাহলে আপনার অ্যাপ বাউন্ডেড মোড থেকে ফুল স্পেস মোডে নির্বিঘ্ন ট্রানজিশনের অনুরোধ করতে পারে এবং তারপরে আপনি বাউন্ডেড মোডে ফিরে যেতে পারেন।
স্পেশিয়াল কন্টেনার যোগ করার মাধ্যমে, Android Android XR নতুন ও বিস্তৃত আইডিয়ার দরজা খুলে দেয়।
ধরা যাক, আপনি নিজের গেমে Android UI নিয়ে আসতে চান অথবা আপনার অ্যাপ্লিকেশনে 3D কন্টেন্ট দেখানোর জন্য কাস্টম শেডার তৈরি করতে চান।
স্পেশিয়াল কন্টেনার চালু হওয়ার আগে, একই সাথে কম্পোজ ও গেম ইঞ্জিন চালানো অসম্ভব ছিল।
এবং রিয়েল-টাইম ইঞ্জিনে অ্যাপ্লিকেশন ডেভেলপমেন্টের দিকে তাকালে, আমরা কার্যকরভাবে দুটি ভিন্ন প্যারাডাইমেও বাস করেছি।
নেটিভ Android ডেভেলপারদের জন্য, আপনার অ্যাপ্লিকেশন রানটাইম Jetpack Compose-এর চারপাশে তৈরি করা হয়।
এটি দ্রুত, প্রতিক্রিয়াশীল UI, ব্যাটারির দক্ষতা, ডায়নামিক লেআউট, সিস্টেম অ্যাক্সেসিবিলিটি এবং নিরবচ্ছিন্ন টেক্সট ইনপুটের জন্য ডিজাইন করা হয়েছে।
কিছু না ঘটলে এটি নিষ্ক্রিয় থাকে এবং এটি অনুমান করা যায়।
গেম ইঞ্জিন ডেভেলপারদের জন্য, ডোমেনটি হল রিয়েল-টাইম রেন্ডারিং, যেখানে Godot এবং Unity-এর মতো টুলগুলি উজ্জ্বল।
তারা শুধু ব্যবহারকারীর ইন্টার্যাকশনের জন্য অপেক্ষা করে না।
এগুলি একটানা টিক ও রেন্ডার লুপ চালায়।
তাই এটি প্রতি সেকেন্ডে ৬০, ৯০, ১০০ ফ্রেম রেট হতে পারে।
এগুলি ফিজিক্স ক্যালকুলেশন, জটিল শেডার শেডার, স্কেলিটাল অ্যানিমেশন, স্পেশিয়াল অডিও এবং এই সব কিছু অবিশ্বাস্য পারফর্ম্যান্সের সাথে করে।
তাহলে, আপনি যদি দু'টি মেম্বারশিপেরই সুবিধা পেতে চান, তাহলে কী হবে?
আগে ডেভেলপারদের এই দুটি পদ্ধতির মধ্যে একটি প্রয়োগ করতে হত।
বিকল্প ক হল Godot বা Unity-র মতো রিয়েল ইঞ্জিনের মধ্যে আপনার সম্পূর্ণ অ্যাপ্লিকেশন আবার লেখা।
আপনার বর্তমান কোডবেস ফেলে দেওয়ার অর্থ হল আপনার নেটিভ কম্পোজ UI-এর অনেক কিছু হারানো, বেসিক টেক্সট ও নেভিগেশন আবার প্রয়োগ করা এবং টেক্সট-টু-স্পিচের মতো Android API-এর জন্য র্যাপার লেখা।
বিকল্প খ হল, আপনার নেটিভ অ্যাপের মধ্যে র Vulkan ও OpenXR API ব্যবহার করে একদম শুরু থেকে লো-লেভেল 3D গ্রাফিক্স রেন্ডারিং পাইপলাইন তৈরি করা।
প্রোডাক্ট ফিচার ডেলিভার করার পরিবর্তে, আপনি মূলত নিজের মালিকানাধীন ইঞ্জিন তৈরি করছেন।
তাই আজ আমি আপনাকে দেখাতে পারি যে কীভাবে আপনি Godot এবং Unity-কে লাইব্রেরি হিসেবে ব্যবহার করে সবচেয়ে বেশি সুবিধা পেতে পারেন।
এবং এর সাথে, স্পেশিয়াল কন্টেনার যোগ করার ফলে, আপনি এখন দুটি জগতেরই সেরা অভিজ্ঞতা পেতে পারেন।
আপনার Jetpack অ্যাপ্লিকেশনের লাইব্রেরির মধ্যে সরাসরি রিয়েল-টাইম ইঞ্জিন এম্বেড করার মাধ্যমে আমরা এটি করতে পারি।
আপনি প্রোডাকশন Android অ্যাপ স্ট্রাকচার, বিজনেস লজিক এবং আপনার অ্যাপ্লিকেশনের মধ্যে 3D স্পেশিয়াল কন্টেন্টের নির্বিঘ্ন রেন্ডারিং বজায় রাখেন।
এর অর্থ হল, সব ফর্ম ফ্যাক্টরের জন্য আপনার কাছে একটি APK থাকতে পারে।
যেখানে আপনি শুধু 3D কন্টেন্ট দেখাতে চান না, সেখানে আপনি অ্যাক্টিভিটি বা ফ্র্যাগমেন্টে লাইব্রেরি লোড না করার বিকল্প বেছে নিতে পারেন এবং আপনার কোডবেস পরিষ্কার রাখতে পারেন এবং আপনাকে অনেক ডিভাইস সাপোর্ট করার অনুমতি দেয়।
দর্শনটি সহজ, অ্যাপ্লিকেশনটি ডিকাপল করুন এবং রিয়েল-টাইম ইঞ্জিনটিকে একটি লাইব্রেরি করুন।
Android অ্যাপ বা ইঞ্জিনের মধ্যে থেকে বেছে নেওয়ার পরিবর্তে, আপনি ইঞ্জিনকে একটি এম্বেড করা সফ্টওয়্যার লাইব্রেরি হিসেবে বিবেচনা করেন, যা AAR ডিপেন্ডেন্সি নামেও পরিচিত।
অন্য যেকোনও লাইব্রেরির মতো আপনি এটি সরাসরি আপনার Gradle বিল্ডে পুল করেন।
আপনার নেটিভ Android কোড 3D কম্পোনেন্ট হোস্ট করা অ্যাপ্লিকেশনের মূল মস্তিষ্ক হিসেবে কাজ করে।
Jetpack Compose XR তার সেরা কাজ করে, নেটিভ অ্যাক্সেসযোগ্য ফ্লুইড UI রেন্ডার করে এবং অ্যাপের অবস্থা ম্যানেজ করে।
এবং যখন একটি 3D এলিমেন্টের প্রয়োজন হয়, তখন অ্যাপটি একটি এম্বেড করা রেন্ডারিং ভিউ হোস্ট করে যা গেম ইঞ্জিন নিজেই রেন্ডার করে।
আজ, আমরা Godot-এর জন্য এটি কীভাবে কাজ করে তা গভীরভাবে জানব।
এবং আমি Kotlin ও GDScript-এর মধ্যে এম্বেডিং ও দ্বিমুখী কমিউনিকেশন স্থাপন করার মেকানিক্স দিয়ে শুরু করব।
এবং তারপরে আমরা নতুন OpenXR এক্সটেনশন, স্পেশিয়াল কন্টেনার দেখব, যা সত্যিই মাল্টিটাস্কিং এবং স্পেশিয়াল লেআউটের নতুন প্রজন্মকে আনলক করবে।
আমি আগে যে ডেমো দেখিয়েছিলাম তা ঝটপট দেখে নেওয়া যাক।
আমি Godot ও Android Studio ব্যবহার করে এটি তৈরি করেছি।
এবং এই ক্লিপে দেখানো হয়েছে যে কীভাবে Jetpack Compose for XR আমাদের স্কোরবোর্ড, বিজ্ঞপ্তি ও হাইলাইটের জন্য নেটিভ 2D UI স্পেশিয়াল প্যানেল রেন্ডার করে।
আর এর উপরে, Godot একটি সকার ম্যাচের সিমুলেশন রেন্ডার করছে।
Kotlin থেকে GDScript-এ আমরা কীভাবে যোগাযোগ করি তার ব্রেকডাউন এখানে দেখতে পাবেন।
এটি করার জন্য, Godot একটি প্লাগ-ইন আর্কিটেকচার প্রদান করে।
আপনি একটি লাইটওয়েট Kotlin ক্লাস লেখেন যা GodotPlugin-কে এক্সটেন্ড করে।
এই ক্লাস JVM এবং Godot-এর C++ রানটাইমের মধ্যে ইন্টারফেস হিসেবে কাজ করে।
এই কারণে, আপনার অ্যাপ এবং Godot-এর মধ্যে ইন্টার্যাকশন খুব খুব হালকা।
Godot-এর আর্কিটেকচার Godot ও Kotlin-এর মধ্যে টু-ওয়ে কমিউনিকেশন সাপোর্ট করে।
Godot-এ কোনও গেম ইভেন্ট ঘটলে, যেমন গোল বা ফাউল, Godot সরাসরি Kotlin ফাংশন কল করতে পারে এবং রিয়েল টাইমে Compose UI আপডেট করতে পারে।
তারপর, ব্যবহারকারী যখন মিডিয়া প্লেয়ারের সাথে ইন্টার্যাক্ট করেন, যেমন, পজ করা বা বিভিন্ন হাইলাইট বেছে নেওয়া, Kotlin তখন Godot-এ সিগন্যাল পাঠায় এবং সংশ্লিষ্ট গেম ইঞ্জিন অ্যাকশন ঘটে।
Godot প্লাগ-ইন ক্রস করা হয়ে গেলে, পারফর্ম্যান্স ও ম্যানেজমেন্ট ডোমেন লাইনের সাথে সামঞ্জস্যপূর্ণ হয়ে যায়।
Android অ্যাপের জন্য সেরা স্ট্যান্ডার্ড পদ্ধতি এবং Godot প্রোজেক্টের জন্য সেরা স্ট্যান্ডার্ড পদ্ধতি প্রয়োগ করুন।
এখন আমরা আর্কিটেকচার দেখব।
আসুন দেখে নেওয়া যাক, কীভাবে আমরা এটি Godot-এ Android অ্যাপে ওয়্যার করি।
Godot ইঞ্জিন রানটাইম Maven-এর মাধ্যমে AAR হিসেবে ডিস্ট্রিবিউট করা হয়।
আপনার বিল্ডে ইন্টিগ্রেট করার জন্য শুধু স্ট্যান্ডার্ড 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")অ্যাসেট প্যাকেজ করা হয়ে গেলে, 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 চালু হলে প্লাগ-ইন কম্পোজ সাইড থেকে রেজিস্টার করা হয়েছে।
সুতরাং, এখন প্লাগ-ইন রেজিস্টার করা হয়েছে, আসুন Godot-এ একটি সিগন্যাল পাঠিয়ে কমিউনিকেট করার জন্য Android সেট-আপ করি।
আমাদের 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-এর সাথে যোগাযোগ করতে পারে, ধরা যাক কোনও গোল হয়েছে, আমরা Godot স্ক্রিপ্ট থেকে সরাসরি Kotlin ফাংশন কল করতে পারি, শুধু প্লাগ-ইন ইনস্ট্যান্স ব্যবহার করে।
func _on_goal_scored(minute: int, home: int, away: int) -> void:
if soccer_plugin:
soccer_plugin.onMatchEvent("GOAL", minute, home, away)তাই প্রথমে, আপনি UsedByGodot-এর সাথে 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)
}
}ইঞ্জিনটি JNI মার্শালিং ম্যানেজ করে।
এবং তারপর আমরা সরাসরি Godot Script থেকে ফাংশনটি কল করতে পারি।
এটি ব্যবহার করার সময় একটি দ্রুত পরামর্শ মনে রাখতে হবে, সেটি হল UsedByGodot-এর জন্য সঠিক স্ট্রিং ম্যাচিং প্রয়োজন।
তাই, আপনার Godot স্ক্রিপ্ট onMatchEvent কল করলে, আপনার Kotlin নাম ঠিক একই হতে হবে, যাতে কোনও অটোমেটিক ক্যামেল কেস বা স্নেক-কেস কনভার্সন না হয়।
তাই, আপনাকে শুধু নিশ্চিত করতে হবে যে দুটি পদ্ধতিই যেন হুবহু এক হয়।
প্লাগ-ইন কানেক্ট করার পরে, এই আলোচনার সবচেয়ে গুরুত্বপূর্ণ অংশ হল, যাকে আমি মেন্টাল মডেল বলি।
এখানেই গেম ইঞ্জিন এবং Jetpack Compose দুটি মৌলিকভাবে, যেমন, বিপরীত আর্কিটেকচারে কাজ করে।
গেম ইঞ্জিন ক্রমাগত ফ্রেম টিকের উপর কাজ করে।
আপনি কিছু পরিবর্তন করুন বা না করুন, গেম ইঞ্জিন টিক প্রসেস প্রতি সেকেন্ডে ৬০, ৯০, ১০০ বার ঘটে, যা সমৃদ্ধ এবং মসৃণ রিয়েল-টাইম রেন্ডারিং প্রদান করে।
এবং Godot-এর মতো ইঞ্জিন GPU এবং CPU সাইকেল কার্যকরভাবে ব্যবহার করে।
অন্যদিকে, Jetpack Compose, রিঅ্যাক্টিভভাবে জিনিসগুলি ম্যানেজ করে এবং এটি স্টেট-চালিত।
এবং যখন কোনও অ্যাপ পরিবর্তন হয় না, তখন কম্পোজ চলতে থাকে, 0 CPU ব্যবহার করে।
আপনি যদি কোনও স্পষ্ট কৌশল ছাড়াই দুটি রানটাইম চালানোর জন্য এগুলিকে কানেক্ট করতে চান, তাহলে এই দর্শনগুলি একে অপরের সাথে সংঘর্ষে লিপ্ত হতে পারে।
তাহলে, আসুন আমরা দেখি- আসুন- আসুন আমরা দেখি যে কীভাবে আমরা এই সমস্যা থেকে মুক্তি পেতে পারি, যাকে আমি টিক লিক বলি।
এখানে আমরা 3D অভিজ্ঞতা পাই, যেমন স্কোরবোর্ড টাইমার।
আপনার ইঞ্জিন স্ক্রিপ্টে, একটি ti- টাইমার ভেরিয়েবেল আছে যা ক্রমাগত পরিবর্তিত হয়।
এবং আপনি যদি এই ভেরিয়েবলটি কম্পোজে অন্ধভাবে ফরওয়ার্ড করেন, ধরা যাক প্রতি সেকেন্ডে ৯০ বার, তাহলে আপনি প্রতি সেকেন্ডে ৯০ বার এই রিকম্পোজিশন লুপে প্রবেশ করতে চলেছেন।
সুতরাং, এর অর্থ হল Compose আপনার কম্পোজেবলগুলি পুনরায় মূল্যায়ন করবে, লেআউটগুলি পুনরায় পরিমাপ করবে, প্রতি সেকেন্ডে ৯০ বার UI ট্রিগুলি পুনরায় আঁকবে।
And you are going to see frame drops, and you're going to see UI lag, and you're going to hit thermal throttling pretty quickly.
তাই, মূল নিয়ম হল, কন্টিনিউয়াস ইঞ্জিন লুপকে কখনও রি-এডি-রিঅ্যাক্টিভ UI লুপকে ডিক্টেট করতে দেবেন না।
এই উদাহরণে, 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 গেম ইঞ্জিন লুপ ক্রমাগত তার নিজস্ব রেন্ডার থ্রেডে এক্সিকিউট করে।
UI, Android UI অবশ্যই 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 Manifest ফাইলে 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-এর নিজস্ব XR প্রোজেক্ট সেটিংস আছে, যেখানে আমি এর এডিটরের মাধ্যমে OpenXR, XR শেডার এবং স্পেশিয়াল কন্টেনার চালু করতে পারি।
Godot-এ স্পেশিয়াল কন্টেনার চালু করার জন্য আপনাকে শুধু এইটুকুই করতে হবে।
সুতরাং, এখন আপনি Android XR, স্পেশিয়াল কন্টেনার এবং এই সমস্ত ডেভেলপ করা সহ Godot-কে কাজ করিয়েছেন, স্পেশিয়াল প্যানেল এবং JXR সহ স্পেশিয়াল কন্টেনার ব্যবহার করার পেশাদার পদ্ধতি, আমরা অবশ্যই ডেভেলপার ওয়েবসাইটে আরও তথ্য চেক করার সাজেশন দিই।
আমরা এইমাত্র Godot Engine এবং Android XR-এর মধ্যে কীভাবে স্পেশিয়াল কন্টেনার ব্যবহার করতে হয় তার একটি উদাহরণ দেখলাম এবং আমি নিশ্চিত যে আপনাদের মধ্যে অনেকেই এমন একজন ডেভেলপারের কাছ থেকে শুনতে চান যিনি তার Android XR অভিজ্ঞতার জন্য স্পেশিয়াল কন্টেনারে সক্রিয়ভাবে কাজ করছেন।
তাই, এরপর, আমি NBA থেকে জোশ এবং রড্রিগোকে আমন্ত্রণ জানাতে চাই, যারা প্রথম আবেদনকারীদের মধ্যে একজন-- প্রথম পার্টনারদের মধ্যে একজন যারা তাদের অ্যাপ্লিকেশনের মধ্যে স্পেশিয়াল কন্টেনার ব্যবহার করছেন।
ধন্যবাদ, লুক।
হ্যালো, সবাইকে।
আমার নাম Josh এবং আমি NBA-তে Android ডেভেলপার হিসেবে কাজ করি।
আজ আমার সাথে আমার সহকর্মী, রড্রিগো সানচেজ আছেন, যিনি Beyond Sports-এ ইউনিটি সফ্টওয়্যার ডেভেলপার হিসেবে কাজ করেন।
আজ আমরা আপনাকে দেখাব যে কীভাবে আমরা NBA অ্যাপের জন্য কোর্ট ভিউ তৈরি করতে লাইব্রেরি হিসেবে Unity ব্যবহার করেছি।
NBA-তে আমরা সবসময় অনুরাগীদের জন্য নতুন অভিজ্ঞতা নিয়ে আসার চেষ্টা করি।
তাই, Google এবং Beyond Sports-এর সাথে যৌথভাবে, আমরা Court View নিয়ে আসতে পেরে আনন্দিত।
কোর্ট ভিউ NBA গেমের 3D রিক্রিয়েশন অফার করে, যা League Pass সাবস্ক্রাইবারদের নতুন দৃষ্টিকোণ থেকে দেখার সুযোগ করে দেয়, যা সাধারণ ব্রডকাস্ট ভিডিওর মাধ্যমে সম্ভব নয়।
কোর্ট স্কেল ও আবার রোটেট করুন, আপনার স্পেসে যেখানে খুশি এটি রাখুন।
এর চারপাশে ঘুরে ঘুরে অনন্য অ্যাঙ্গেল থেকে গেমটি দেখুন।
এই পুরো সময় জুড়ে, আপনি নিজের NBA অ্যাপ স্বাভাবিকভাবেই ব্যবহার করতে পারবেন, সিঙ্ক করা পরিসংখ্যান দেখা থেকে শুরু করে লাইভ ব্রডকাস্ট দেখা পর্যন্ত।
এইসব কীভাবে সেট-আপ করা হয়?
কোর্ট ভিউ Unity-তে রেন্ডার করা হয় এবং লাইব্রেরি হিসেবে Unity ব্যবহার করে আমাদের Android অ্যাপে দেখানো হয়।
তারপর আমরা Maven Central-এ হোস্ট করা একটি Android আর্কাইভ তৈরি করি, যা NBA অ্যাপ একটি ডিপেন্ডেন্সি হিসেবে ব্যবহার করতে পারে।
এর ফলে আমরা Android ও Unity-র মধ্যে কাজ ভাগ করে নিতে পেরেছি।
তাই আমরা প্রত্যেকেই নিজেদের সেরাটা দিতে পারি এবং সামগ্রিকভাবে ডেভেলপমেন্টের গতি বাড়াতে সমান্তরালভাবে কাজ করতে পারি।
উদাহরণস্বরূপ, আমাদের টিম যখন কম্পোজে ফুল-স্পেস অভিজ্ঞতা নিয়ে কাজ করছিল, তখন Unity টিম 3D কোর্টের স্কেলিং এবং লেআউটের উপর ফোকাস করতে পেরেছিল।
এখন, আমি রড্রিগোকে Unity প্রোজেক্ট এবং তারা কীভাবে লাইব্রেরি হিসেবে Unity-র সাথে কাজ করেছে সেই সম্পর্কে আরও কথা বলতে দেব।
ধন্যবাদ, জোশ।
Beyond Sports-এ আমরা কিছু বছর ধরে NBA-এর সাথে একসাথে কাজ করছি যাতে ম্যাচের আসল ডেটা ব্যবহার করে 3D ভিজ্যুয়ালাইজেশন দেখানো যায়।
এই প্রোজেক্টের মাধ্যমে, আমরা আমাদের 3D সিমুলেশনকে Android XR-এর সাথে মানানসই করে তুলেছি।
আমাদের সিমুলেশন C# সহ Unity ইঞ্জিনে তৈরি করা হয়েছে।
আমরা লাইব্রেরি হিসেবে Unity-এর সাথে Android-এ সিমুলেশন এক্সপোর্ট করি এবং Kotlin ব্যবহার করে এর পাশে একটি কমিউনিকেশন লেয়ার তৈরি করি।
এই দুটি মডিউলকে একসাথে একটি Maven ডিপেন্ডেন্সি হিসেবে বান্ডেল করার আগে, NBA-কে তাদের অ্যাপ্লিকেশনে অন্তর্ভুক্ত করতে হবে।
স্ক্রিনে, আপনি একই সিমুলেশনের একটি ছোট ভিডিও দেখতে পাবেন যা জোশ আগে দেখিয়েছিলেন, শুধু এডিটরে চলছে।
এটি করার জন্য, আমরা Unity-এর জন্য Android XR এক্সটেনশন প্যাকেজ ব্যবহার করি।
আমি আপনাকে প্রসেসটি বুঝিয়ে দিচ্ছি।
তাই, Unity-তে বিল্ড করার আগে, আপনি সঠিক সেটিংস ও প্লাগ-ইন বেছে নিয়েছেন কিনা তা দেখে নিন।
Unity-র XR প্লাগ-ইন ম্যানেজমেন্ট প্যাকেজ এবং Unity প্যাকেজের জন্য Google-এর Android XR এক্সটেনশন পান।
OpenXR Android XR এক্সটেনশন প্রোভাইডার ব্যবহার করতে, XR প্লাগ-ইন ম্যানেজমেন্ট সেটিংসে সেগুলি কনফিগার করুন।
আমাদের ব্যবহারের ক্ষেত্রে, আমরা স্পেশিয়াল কন্টেনার ব্যবহার করতে চেয়েছিলাম, তাই আমরা সেই ফিচার চালু করেছি এবং সেশন ম্যানেজমেন্ট সেটিংস থেকে ইমারসিভ XR বন্ধ করেছি।
আমরা কোনও XR ইনপুট প্যাকেজ ব্যবহার করিনি, কারণ সমস্ত ইনপুট Unity-এর বাইরে নেটিভভাবে ম্যানেজ করা হয়।
এরপর, প্লেয়ার সেটিংসে, আপনার প্রোজেক্টকে ন্যূনতম API লেভেল ২৪ বা তার বেশি ব্যবহার করার জন্য সেট করতে হবে এবং অ্যাপ্লিকেশন এন্ট্রি পয়েন্টটি অবশ্যই নতুন গেম অ্যাক্টিভিটিতে সেট করতে হবে।
মনে রাখবেন যে অ্যাক্টিভিটি আনচেক করা থাকতে হবে।
আমরা শুধু ARM64-এর জন্য তৈরি করতে চাই, তাই আমরা সেই টার্গেট আর্কিটেকচার বেছে নিই এবং রেজোলিউশন বিভাগে, আমরা ব্যাকগ্রাউন্ডে রান করার বিকল্প চালু করেছি, যাতে আপনি অন্যান্য অ্যাপের সাথে মাল্টিটাস্ক করতে পারেন এবং পাশাপাশি 3D সিমুলেশন চালু রাখতে পারেন।
এছাড়াও, আমরা Resizeable Activity চালু করি, কারণ সিস্টেমের অনুমতির অনুরোধের মতো ডায়ালগ রেন্ডার করার জন্য এটি প্রয়োজন।
সবশেষে, Android XR অ্যাপের ক্ষেত্রে ইউনিভার্সাল রেন্ডার পাইপলাইন ও Vulkan গ্রাফিক্স API ব্যবহার করে রেন্ডার করার সাজেশন দেওয়া হয়।
আপনার URP অ্যাসেটে, HDR এবং পোস্ট-প্রসেসিং বন্ধ করা উচিত।
এইসব ধাপ সম্পূর্ণ করার পরে, আপনি বিল্ড তৈরি করার জন্য রেডি।
বিল্ড উইন্ডোতে যান এবং আপনি 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")
}
}আমাদের প্রোজেক্টে চারটি আলাদা মডিউল আছে: NBA-এর র্যাপিং অ্যাপ্লিকেশনের মক-আপ প্লেসহোল্ডার হিসেবে টেস্ট করার জন্য মূল APK মডিউল, আমাদের এক্সপোর্ট করা বিল্ড ধারণকারী Unity লাইব্রেরি মডিউল, অ্যাক্টিভিটির মধ্যে আমাদের তৈরি করা কমিউনিকেশন লেয়ার ধারণকারী nativeCommunications মডিউল এবং Maven-এ ডেলিভার করার জন্য আমাদের AAR-কে একটি প্যাকেজে মার্জ করার জন্য ব্যবহৃত packageExporter মডিউল।
Android Fused Library বা Fat AAR-এর মতো পুরনো কমিউনিটি পদ্ধতি ব্যবহার করে একাধিক AAR মার্জ করা যেতে পারে।
Unity এবং মূল অ্যাক্টিভিটি একই প্রসেসে চালিয়ে আপনি স্ট্রাকচার ও কমিউনিকেশন সহজ রাখতে পারেন।
আমরা এই বিকল্পটি বেছে নিইনি এবং Unity অ্যাক্টিভিটিকে নিজস্ব প্রসেসে আলাদা করার সিদ্ধান্ত নিয়েছি। এর ফলে, Unity অংশটি ক্র্যাশ করলেও NBA অ্যাপ প্রভাবিত হবে না এবং ক্র্যাশ না করেই Unity বন্ধ করার প্রসেসটি নিরাপদে ম্যানেজ করতে পারবে।
এই সিদ্ধান্তের ফলে আমাদের আরও শক্তিশালী ইন্টার-প্রসেস কমিউনিকেশন সিস্টেম গ্রহণ করতে হবে।
অ্যাক্টিভিটিগুলি বিভিন্ন প্রসেসে থাকার কারণে, তাদের মধ্যে কোনও মেমরি শেয়ার করা হয় না এবং অনেক সাধারণ Android IPC পদ্ধতি কাজ করে না বা প্রয়োজনীয়তা পূরণ করতে পারে না।
NBA-এর মূল অ্যাক্টিভিটি এবং আমাদের এম্বেড করা ইউনিটি অ্যাক্টিভিটির মধ্যে কথা বলার জন্য আমরা Android ইন্টারফেস ডেফিনিশন ল্যাঙ্গুয়েজ বা AIDL ব্যবহার করি।
তারপর, Android ও Unity-এর মধ্যে যোগাযোগও ইন্টারফেসের উপর ভিত্তি করে হয়, যা C# ইন্টারফেস ইমপ্লিমেন্টেশনে Kotlin ইন্টারফেস রেজিস্টার করার জন্য Unity-তে বিল্ট-ইন AndroidJavaProxy ব্যবহার করে।
আমাদের কমিউনিকেশন মডিউল মূলত অ্যাক্টিভিটি ব্রিজের উপর ফোকাস করে।
// 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) }
}
}দুটি অ্যাক্টিভিটিরই অ্যাক্টিভিটি ব্রিজ এবং এর মধ্যে থাকা ইউটিলিটি পদ্ধতিতে অ্যাক্সেস আছে।
ব্রিজে এমন একটি পরিষেবার রেফারেন্স থাকে যা আমাদের সংজ্ঞায়িত AIDL ইন্টারফেসগুলি বাস্তবায়ন করে এবং এটি অ্যাক্টিভিটিগুলিকে প্রসেসের সাথে, দুঃখিত, পরিষেবার সাথে বাইন্ড করার একটি সহজ উপায় প্রদান করে এবং অভ্যন্তরীণভাবে এর লাইফসাইকেল ম্যানেজ করে।
কলব্যাকের মাধ্যমে সিমুলেশন শুরু করা যেতে পারে, যেখানে আমরা প্যারামিটার হিসেবে ম্যাচ আইডি পাস করি।
তারপর, ব্রিজটি AIDL-এর মাধ্যমে এবং তারপর 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 বুট লোডিং ও ইনিশিয়ালাইজ করা হয়ে গেলে এবং আমরা নিরাপদ অবস্থায় আছি বলে মনে করলে, Unity যে রেডি তা C# থেকে কমিউনিকেট করি।
// 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 অ্যাপে কোর্ট ভিউ ইউনিটি প্যাকেজ কীভাবে ইন্টিগ্রেট করা হয় সেই সম্পর্কে আরও কথা বলতে পারেন।
আমাদের Jetpack Compose অ্যাপের সাথে লাইব্রেরি হিসেবে Unity ইন্টিগ্রেট করতে আমরা এইসব Gradle ডিপেন্ডেন্সি ব্যবহার করি: 3D সেশন অ্যাক্সেস করার জন্য XR রানটাইম, আমাদের Unity অ্যাক্টিভিটি হোস্ট করতে এবং স্পেশিয়াল এনভায়রনমেন্ট টগল করতে Jetpack SceneCore এবং আমাদের Compose কোডে স্পেশিয়াল প্যানেল অ্যাক্সেস করতে XR-এর জন্য 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")
}এরপরে, Jetpack Compose-এ কীভাবে Court View রেন্ডার করা হয় তা দেখা যাক।
// 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-র স্পেশিয়াল প্যানেল দেখানোর জন্য এটি প্রয়োজন।
এরপর, নির্দিষ্ট গেমের জন্য কোর্ট ভিউ লঞ্চ করতে আমরা AIDL-এর মাধ্যমে গেমের তথ্য পাঠাই।
তারপর Unity চালু হয়।
আমাদের Android আর্কাইভ এটিকে Unity অ্যাক্টিভিটিতে ইনটেন্ট হিসেবে পাঠায় এবং আমরা এটিকে SpatialActivityPanelController-এ র্যাপ করি।
সেখান থেকে, আমরা শুধু সেই কন্ট্রোলারকে SpatialActivityPanel-এ পাস করি।
এই প্যানেলে কোর্ট ভিউ থাকে এবং এটি আমাদের মূল স্পেশিয়াল প্যানেলের সাথে আমাদের সাবস্পেসে শেয়ার করা হয়।
এর ফলে আমরা কোনও সমস্যা ছাড়াই একসাথে দুটিই নিয়ন্ত্রণ করতে পারি।
আর এইটুকুই।
আমি এটি ল্যুককে ফেরত পাঠাব।
আপনাদের ধন্যবাদ জানাই।
আপনি দেখতে পাচ্ছেন, NBA, Unity এবং Jetpack Compose ডেভেলপমেন্টকে বিভক্ত করে Court View-এর গতি বাড়াতে সক্ষম হয়েছে।
স্পেশিয়াল কন্টেনার ব্যবহার শুরু করা এবং আপনার নিজের Android XR অ্যাপে এটি ব্যবহার করার বিষয়ে আরও জানতে, developer.android.com/develop/xr-এ ডেভেলপার ডকুমেন্টেশন দেখুন।
আজ আমাদের সাথে যোগ দেওয়ার জন্য আপনাকে অনেক ধন্যবাদ এবং আপনি কী তৈরি করেন তা দেখার জন্য আমরা অধীর আগ্রহে অপেক্ষা করছি।
androidResources {
ignoreAssetsPattern = "!.svn:!.git:!.gitignore:!.ds_store:!*.scc:<dir>_*:!CVS:!*~:!android:!export_presets.cfg"
}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")@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 }
)
}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)
}
}
}class MainActivity : ComponentActivity(), GodotHost {
override fun getHostPlugins(engine: Godot): Set<GodotPlugin> =
setOf(SoccerGodotPlugin(engine))
}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),
)
}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)class SoccerGodotPlugin(godot: Godot) : GodotPlugin(godot) {
fun sendCommandToGodot(command: String) {
emitSignal("match_command", command)
}
}func _on_goal_scored(minute: int, home: int, away: int) -> void:
if soccer_plugin:
soccer_plugin.onMatchEvent("GOAL", minute, home, away)@UsedByGodot
fun onMatchEvent(type: String, minute: Int, home: Int, away: Int, title: String) {
_uiState.update { state ->
state.copy(homeScore = home, awayScore = away, matchSeconds = minute)
}
}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, ...) }
}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
)
}<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="" />// 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")
}
}// 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) }
}
}public class NativeInterfaceListener : AndroidJavaProxy {
[UsedImplicitly]
public void startMatch(string matchID) {
UnityThreadHelper.InvokeOnMain(() => _internalAPI.StartMatch(matchID));
}
}fun onStartMatchClickedInternal() {
if (isServiceBound) {
AIDLBridgeService.startMatch("match_id")
} else {
delayedCallbacksQueue.add(action)
}
}// 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) }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")
}// 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)
}
}
}