Oleh Abe Haskins (Twitter, Github)

Dalam artikel ini, kita akan mendalami menggunakan Unity3D dan TensorFlow untuk mengajarkan AI melakukan tugas sederhana dalam game: memasukkan bola ke dalam ring. Kode sumber lengkapnya tersedia di Github, bila Anda memiliki pertanyaan, silakan hubungi saya di Twitter.


Sebuah Pengantar untuk Game kami

Ada sebuah game di mana pemain memiliki satu tujuan utama: memasukkan bola ke dalam keranjang. Ini tidak terdengar sulit, tetapi ketika darah Anda terpompa, jantung Anda berdegup kencang, penonton bersorak — yap, sangatlah sulit melakukan tembakan itu. Apakah saya berbicara tentang permainan Bola Basket Amerika klasik? Tidak, tidak pernah mendengarnya. Saya sedang berbicara tentang game arkade Midway klasik NBA Jam.
Jika Anda pernah memainkan NBA Jam atau salah satu game yang terinspirasi olehnya (termasuk real life NBA league, yang saya pikir muncul setelah NBA Jam), maka Anda tahu mekanik untuk menembak bola, dari perspektif pemain, cukuplah sederhana. Anda menahan dan melepaskan tombol shoot dengan pengaturan waktu yang tepat. Namun, pernahkah Anda bertanya-tanya bagaimana tembakan ini dilakukan dari perspektif game? Bagaimana cara memilih lengkungan bola? Seberapa keras bola dilemparkan? Bagaimana cara komputer mengetahui sudut untuk membidik?
Bila Anda adalah orang yang cerdas dan cenderung matematis, Anda mungkin dapat menemukan jawabannya dengan pena dan kertas, tetapi, penulis entri blog ini gagal aljabar kelas 8, jadi... jawaban “orang cerdas” itu tidaklah mungkin. Saya harus melakukan pendekatan dengan cara yang berbeda.
Daripada mengambil jalur yang lebih sederhana, cepat, dan efisien dengan melakukan perhitungan matematis yang diperlukan untuk melakukan tembakan, kita akan melihat seberapa dalam lubang kelinci, mempelajari beberapa TensorFlow sederhana, dan mencoba membidik beberapa ring.

Memulai

Kita membutuhkan beberapa hal untuk menjalankan project ini.
Bila Anda bukan ahli dalam setiap teknologi ini, tidak menjadi masalah! (Saya juga bukan ahli dalam semua bidang ini!) Saya akan melakukan yang terbaik untuk menjelaskan bagaimana semua bagian ini akan cocok ketika bekerja bersama. Salah satu kelemahan menggunakan begitu banyak variasi teknologi adalah saya tidak akan bisa menjelaskan semuanya secara detail, tetapi saya akan mencoba untuk menautkannya ke sumber daya edukasi sebanyak mungkin!

Download Project

Saya tidak akan mencoba membuat ulang project ini selangkah demi selangkah, jadi saya sarankan untuk mendownload kode sumber di Github dan selalu mengikuti ketika saya menjelaskan apa yang terjadi.
Catatan: Anda perlu mendownload impor package aset ML-Agents Unity untuk Tensorflow agar bisa digunakan di C#. Bila Anda mendapatkan error terkait Tensorflow yang tidak ditemukan di Unity, pastikan Anda mengikuti dokumen setup Unity untuk TensorflowSharp.

Apa tujuan kami?

Agar tetap sederhana, hasil yang kami inginkan untuk project ini sangatlah sederhana. Kami ingin memecahkan: jika penembak berjarak X dari ring, tembak bola dengan kekuatan Y. Itu saja! Kami tidak akan mencoba mengarahkan bola atau melakukan sesuatu yang fantastis. Kami hanya mencoba mencari tahu seberapa keras melempar bola agar tembakannya masuk.
Bila Anda tertarik mengenai cara membuat AI yang lebih kompleks dalam Unity, Anda harus mengecek project ML-Agents yang jauh lebih lengkap dari Unity. Metode yang akan saya bicarakan di sini dirancang sederhana, mudah diterapkan, dan tidak selalu menunjukkan praktik terbaik (saya juga belajar!)
Pengetahuan saya yang terbatas tentang TensorFlow, machine learning, dan matematika tidak akan kentara. Jadi, pertimbangkan dan pahamilah bahwa semua ini hanya untuk bersenang-senang.

Keranjang dan Bola

Kami sudah membahas inti dari tujuan kami: yaitu membidik keranjang. Untuk memasukkan bola ke dalam keranjang, Anda memerlukan keranjang dan… tentu saja, bola. Di sinilah Unity masuk.
Bila Anda tidak familier dengan Unity, ketahuilah bahwa ini adalah engine game yang memungkinkan Anda build game 2D dan 3D untuk semua platform. Unity memiliki fisika bawaan, pemodelan 3D dasar, dan runtime skrip yang sangat bagus (Mono) yang memungkinkan kita menulis game di C#.
Saya bukan artis, tetapi saya menarik beberapa blok dan menyusun adegan ini.


Blok merah itu tentu saja adalah pemain kita. Ring telah diatur dengan pemicu tak terlihat yang memungkinkan kita mendeteksi ketika suatu objek (bola) melewati ring itu.


Dalam editor Unity Anda bisa melihat pemicu tidak terlihat yang tergambar dengan warna hijau. Anda akan melihat dua pemicu. Ini untuk memastikan bahwa kita hanya menghitung bola yang masuk keranjang ketika bola jatuh dari atas ke bawah.
Bila kita melihat metode OnTriggerEnter di /Assets/BallController.cs (skrip yang dimiliki setiap instance dari bola basket kita), Anda bisa melihat bagaimana kedua pemicu ini digunakan bersama.

Fungsi ini melakukan beberapa hal. Pertama, ia memastikan bahwa pemicu atas dan bawah teraktifkan, kemudian ia mengubah warna bola sehingga secara visual kita bisa melihat bola yang masuk ke ring, dan yang terakhir, ia merekam log dua variabel kunci yang kita perlukan, distance dan force.y.

Melakukan Tembakan

Buka /Assets/BallSpawnerController.cs. Ini adalah skrip yang aktif di penembak kami dan melakukan tugas untuk memunculkan Bola basket dan mencoba melakukan tembakan. Lihat cuplikan ini di dekat akhir metode DoShoot().

Kode Instantiates ini membuat instance baru dari bola, kemudian menyetel kekuatan bidikan dan jarak dari sasaran (jadi nanti kita bisa merekam log-nya dengan lebih mudah, seperti yang kami tunjukkan di cuplikan terakhir).
Bila /Assets/BallController.cs masih terbuka, Anda bisa melihat metode Start() kami. Kode ini akan dipanggil saat kita membuat bola basket baru.

Dengan kata lain, kita membuat bola baru, memberinya kekuatan, kemudian secara otomatis menghancurkan bola setelah 30 detik karena kita akan berhadapan dengan banyak bola dan kita harus memastikan serta menjaga agar jumlah bolanya tetap masuk akal.
Mari coba jalankan semuanya dan lihat bagaimana kinerja penembak terbaik kita. Anda bisa menekan tombol ▶️ (Play) di editor Unity dan kita akan melihat...


Pemain kita, yang dikenal sebagai “Red”, hampir siap untuk melawan Steph Curry.
Jadi mengapa Red begitu buruk? Jawabannya terletak pada satu baris dalam Assets/BallController.cs yang bertulis float force = 0.2f. Baris ini membuat klaim tegas bahwa setiap tembakan harus sama persis. Anda akan melihat bahwa Unity mengikuti klaim “sama persis” ini secara harfiah. Objek yang sama, dengan kekuatan yang sama, diduplikat lagi dan lagi akan selalu memantul dengan cara yang sama persis. Rapi.
Ini, tentu saja, bukan yang kita inginkan. Kita tidak akan pernah belajar membidik seperti Lebron bila kita tidak mencoba sesuatu yang baru, jadi mari kita berikan sedikit bumbu.

Mengacak Bidikan, Mengumpulkan Data

Kita bisa memasukkan beberapa arah acak hanya dengan mengubah kekuatan secara acak.

Ini akan membaurkan tembakan sehingga akhirnya kita bisa melihat bagaimana tampilannya ketika bola berhasil masuk keranjang, bahkan jika diperlukan waktu beberapa saat untuk menebak dengan benar.


Red sangat bodoh, tembakannya hanya sesekali masuk, itu pun murni karena keberuntungan. Tidak masalah. Pada titik ini, setiap tembakan yang dilakukan adalah titik data yang bisa kita gunakan. Kita akan membahasnya sebentar lagi.
Sementara itu, kita tentunya tidak ingin hanya bisa menembak dari satu tempat. Kita menginginkan Red berhasil menembak (saat dia cukup beruntung) dari jarak berapa pun. Dalam Assets/BallSpawnController.cs, cari baris ini dan hapus komentar MoveToRandomDistance().

Jika kita menjalankannya, kita akan melihat Red dengan antusias melompat-lompat di sekitar lapangan setelah setiap tembakan.


Kombinasi gerakan acak dan kekuatan acak ini menciptakan satu hal yang sangat menakjubkan: data. Bila Anda memerhatikan konsol di Unity, Anda dapat melihat data dari setiap tembakan yang berhasil masuk akan di-log.


Setiap tembakan yang berhasil merekam log # jumlah tembakan yang berhasil sejauh ini, jarak dari ring, dan kekuatan yang diperlukan untuk melakukan tembakan. Ini sedikit terlalu lambat, mari kita percepat. Kembali ke tempat kita menambahkan panggilan MoveToRandomDistance() dan ubah 0.3f (jeda 300 milidetik setiap tembakan) menjadi 0.05f (jeda 50 milidetik).

Sekarang tekan play dan saksikan tembakan sukses kita semakin banyak.


Nah, begitulah cara latihan yang bagus! Kita bisa melihat dari penghitung di belakang bahwa kami berhasil memasukkan sekitar 6,4% tembakan. Steph Curry, tentu saja tidak. Berbicara tentang pelatihan, apakah kita benar-benar belajar sesuatu dari hal ini? Di mana TensorFlow? Mengapa ini menarik? Oke, itulah langkah berikutnya. Kita sekarang siap untuk mengambil data ini dari Unity dan mem-build model guna memprediksi kekuatan yang diperlukan.

Prediksi, Model, dan Regresi

Memeriksa data kita di Google Spreadsheet
Sebelum kita masuk ke TensorFlow, saya ingin melihat data sehingga saya membiarkan Unity berjalan hingga Red berhasil memasukkan sekitar 50 tembakan. Bila Anda melihat di direktori akar project Unity, Anda akan melihat file baru yang bernama successful_shots.csv. Ini adalah keluaran mentah, dari Unity, dari setiap tembakan sukses yang kita lakukan! Saya meminta Unity mengekspor ini sehingga saya bisa menganalisisnya dengan mudah dalam spreadsheet.
File .csv hanya memiliki tiga baris index, distance dan force. Saya mengimpor file ini di Google Spreadsheet dan membuat Scatterplot dengan garis tren sehingga memungkinkan kita mendapatkan gambaran mengenai distribusi data.


Wow! Lihatlah itu. Saya bersungguh-sungguh, lihat itu. Maksudku, wow... Baiklah, saya mengakui, saya juga tidak yakin apa artinya ini. Biarkan saya merinci apa yang kita lihat
Grafik ini menunjukkan serangkaian titik yang diposisikan sepanjang sumbu Y berdasarkan kekuatan tembakan dan sumbu X berdasarkan jarak dilakukannya tembakan. Apa yang kita lihat adalah korelasi yang sangat jelas antara kekuatan yang diperlukan dan jarak dilakukannya tembakan (dengan beberapa pengecualian acak yang dikarenakan pantulan liar).
Secara praktis Anda bisa membacanya “TensorFlow akan sangat baik menangani hal ini.”
Meskipun kasus penggunaannya sederhana, salah satu hal menakjubkan tentang TensorFlow adalah kita bisa build model yang lebih kompleks bila kita menginginkannya, dengan menggunakan kode yang sama. Misalnya, dalam game full, kita bisa menyertakan fitur — seperti pemosisian dari permainan lainnya, dan statistik tentang seberapa sering mereka memblok tembakan pada masa lalu — untuk menentukan apakah pemain kita harus menembak, atau mengoper.
Membuat TensorFlow.js model kita
Buka file tsjs/index.js di editor favorit Anda. File ini tidak terkait dengan Unity dan hanya skrip untuk melatih model kita berdasarkan data di successful_shots.csv.
Berikut adalah seluruh metode yang melatih dan menyimpan model kita...

Seperti yang Anda lihat, tidak banyak di sini. Kita memuat data dari file .csv dan membuat serangkaian titik X dan Y (terdengar sangat mirip dengan Google Spreadsheet kita di atas!). Dari sana kita meminta model untuk “mencocokkan” dengan data ini. Setelah itu, kita menyimpan model untuk konsumsi di masa mendatang!
Sayangnya, TensorFlowSharp tidak mengharapkan model dalam format yang bisa disimpan oleh Tensorflow.js. Jadi, kita harus melakukan penerjemahan magis agar dapat menarik model ke dalam Unity. Saya telah menyertakan beberapa utilitas untuk membantu hal ini. Proses yang biasa dilakukan adalah kita menerjemahkan model dari TensorFlow.js Format ke Keras Format kita bisa membuat checkpoint yang akan kita gabungkan dengan Protobuf Graph Definition untuk mendapatkan Frozen Graph Definition yang dapat kita tarik ke dalam Unity.
Untungnya, bila Anda hanya ingin bermain-main, Anda bisa melewati semuanya dan menjalankan tsjs/build.sh dan jika semua berjalan lancar, ia akan secara otomatis melalui semua langkah dan memasukkan model frozen di Unity.
Di dalam Unity, kita bisa melihat GetForceFromTensorFlow() di Assets/BallSpawnController.cs untuk melihat apa yang berinteraksi dengan model kita.

Saat Anda membuat definisi grafik, Anda mendefinisikan sebuah sistem kompleks yang memiliki beberapa langkah. Dalam kasus ini, kami mendefinisikan model sebagai layer padat tunggal (dengan layer masukan implisit) yang berarti model kami mengambil satu masukan dan memberi kami beberapa keluaran.
Ketika Anda menggunakan model.predict di TensorFlow.js, ia akan secara otomatis memberikan masukan ke node grafik masukan yang tepat dan memberikan Anda keluaran dari node yang tepat setelah perhitungan selesai. Namun, TensorFlowSharp bekerja secara berbeda dan mengharuskan kita berinteraksi secara langsung dengan node grafik melalui namanya.
Dengan mempertimbangkan hal tersebut, ini adalah persoalan memasukkan data masukan ke dalam format yang diharapkan grafik kita dan mengirim kembali keluarannya ke Red.

Waktunya nge-game!

Menggunakan sistem di atas, saya membuat beberapa variasi pada model kami. Di sini Red melemparkan bola menggunakan model yang dilatih dengan hanya 500 tembakan sukses.


Kami melihat peningkatan 10 kali lipat dalam keberhasilan bola masuk keranjang! Apa yang terjadi bila kami melatih Red selama beberapa jam dan mengumpulkan 10 ribu atau 100 ribu tembakan sukses? Tentu hal tersebut akan meningkatkan permainannya lebih jauh lagi! Nah, saya akan menyerahkan hal tersebut kepada Anda.
Saya sangat menyarankan Anda mengecek kode sumber di Github dan men-tweet saya bila Anda bisa mengalahkan tingkat keberhasilan 60% (spoiler: mengalahkan 60% adalah 100% mungkin, kembali dan lihat gif pertama untuk melihat seberapa baik Anda dapat melatih Red!)