Membuat game kontrol senyum

Halo! Nama saya Ivan Shafran, saya baru saja bergabung dengan tim video VK sebagai programmer pengembang untuk Android. Saya berpartisipasi dalam pembuatan aplikasi produk dan SDK. Dari waktu ke waktu saya mengunjungi hackathon di mana Anda dapat menerapkan ide-ide gila. Hari ini saya akan memberi tahu Anda cara membuat prototipe game seluler dengan kontrol yang tidak biasa dalam beberapa jam: karakter akan bereaksi terhadap senyuman dan kedipan.







Bagaimana ide itu muncul



Ide untuk membuat game seperti itu muncul tepat saat hackathon berlangsung. Formatnya diasumsikan ada satu hari kerja untuk pembangunan, yaitu 8 jam. Untuk membuat prototipe tepat waktu, saya memilih Android SDK. Mungkin mesin game akan lebih cocok, tapi saya tidak memahaminya.



Konsep mengendalikan dengan bantuan emosi disarankan oleh permainan lain: di sana, gerakan karakter dapat diatur dengan mengubah volume suara Anda. Mungkin seseorang telah menggunakan emosi untuk mengontrol permainan. Tapi saya tahu beberapa contoh seperti itu, jadi saya memilih format ini.



Waspadai video keras!




Menyiapkan lingkungan pengembangan



Kami hanya membutuhkan Android Studio di komputer. Jika tidak ada perangkat Android asli untuk dijalankan, Anda dapat menggunakan emulator dengan webcam diaktifkan .



Buat proyek dengan ML Kit







ML Kit adalah alat yang hebat untuk mengesankan juri hackathon: Anda menggunakan AI dalam prototipe! Secara umum, ini membantu untuk menyematkan solusi berdasarkan pembelajaran mesin ke dalam proyek, misalnya, fungsionalitas untuk mengidentifikasi objek dalam bingkai, terjemahan, dan pengenalan teks.



Penting bagi kami bahwa ML Kit memiliki API offline gratis untuk mengenali senyum dan mata terbuka atau tertutup.



Sebelumnya, untuk membuat proyek apa pun dengan ML Kit, Anda harus mendaftar terlebih dahulu di konsol Firebase . Langkah ini sekarang dapat dilewati untuk fungsionalitas offline.



Aplikasi Android



Hapus yang tidak perlu



Agar tidak menulis logika untuk bekerja dengan kamera dari awal, mari kita ambil sampel resmi dan hapus darinya apa yang tidak kita butuhkan.







Pertama, unduh contoh dan coba jalankan. Jelajahi mode Deteksi wajah: ini akan terlihat seperti pratinjau artikel.



Manifesto



Mari mulai mengedit AndroidManifest.xml. Hapus semua tag aktivitas kecuali yang pertama. Dan sebagai gantinya kami akan meletakkan CameraXLivePreviewActivity untuk segera memulai dari kamera. Dalam nilai atribut android: value, kita hanya menyisakan wajah untuk mengecualikan sumber daya yang tidak diperlukan dari APK.



<meta-data
 android:name="com.google.mlkit.vision.DEPENDENCIES"
  android:value="face"/>
<activity
  android:name=".CameraXLivePreviewActivity"
  android:exported="true"
  android:theme="@style/AppTheme">
  <intent-filter>
      <action android:name="android.intent.action.MAIN"/>
      <category android:name="android.intent.category.LAUNCHER"/>
  </intent-filter>
</activity>


Perbedaan langkah penuh.



Kamera



Mari menghemat waktu - kami tidak akan menghapus file yang tidak perlu, sebagai gantinya kami akan fokus pada elemen layar CameraXLivePreviewActivity.



  • Pada baris 117, setel mode deteksi wajah:

    private String selectedModel = FACE_DETECTION;
  • Pada baris 118, hidupkan kamera depan:

    private int lensFacing = CameraSelector.LENS_FACING_FRONT;
  • Di akhir metode onCreate pada baris 198-199, sembunyikan pengaturan

    findViewById( R.id.settings_button ).setVisibility( View.GONE );
    findViewById( R.id.control ).setVisibility( View.GONE );


Kita bisa berhenti disini. Tetapi jika rendering FPS dan grid wajah secara visual mengganggu, Anda dapat mematikannya seperti ini:



  • Di file VisionProcessorBase.java, hapus baris 213-215 untuk menyembunyikan FPS:

    graphicOverlay.add(
           new InferenceInfoGraphic(
              graphicOverlay, currentLatencyMs, shouldShowFps ? framesPerSecond : null));
  • Di file FaceDetectorProcessor.java, hapus baris 75–78 untuk menyembunyikan mesh wajah:

    for (Face face : faces) {
        graphicOverlay.add(new FaceGraphic(graphicOverlay, face));
        logExtrasForTesting(face);
    }


Perbedaan langkah penuh.



Mengenali emosi



Deteksi senyum dinonaktifkan secara default, tetapi mudah untuk memulainya. Bukan tanpa alasan kami mengambil kode contoh sebagai dasar! Mari pilih parameter yang kita butuhkan ke dalam kelas terpisah dan nyatakan antarmuka pendengar:



FaceDetectorProcessor.java

//   FaceDetectorProcessor.java
public class FaceDetectorProcessor extends VisionProcessorBase<List<Face>> {
    public static class Emotion {
        public final float smileProbability;
        public final float leftEyeOpenProbability;
        public final float rightEyeOpenProbability;
        public Emotion(float smileProbability, float leftEyeOpenProbability, float rightEyeOpenProbability) {
           this.smileProbability = smileProbability;
            this.leftEyeOpenProbability = leftEyeOpenProbability;
           this.rightEyeOpenProbability = rightEyeOpenProbability;
        }
    }
    public interface EmotionListener {
        void onEmotion(Emotion emotion);
    }
    private EmotionListener listener;
    public void setListener(EmotionListener listener) {
       this.listener = listener;
    }
    
    @Override
    protected void onSuccess(@NonNull List<Face> faces, @NonNull GraphicOverlay graphicOverlay) {
        if (!faces.isEmpty() && listener != null) {
            Face face = faces.get(0);
            if (face.getSmilingProbability() != null &&
                    face.getLeftEyeOpenProbability() != null && face.getRightEyeOpenProbability() != null) {
                listener.onEmotion(new Emotion(
                        face.getSmilingProbability(),
                        face.getLeftEyeOpenProbability(),
                        face.getRightEyeOpenProbability()
                ));
            }
        }
    }
}


Untuk mengaktifkan klasifikasi emosi, atur FaceDetectorProcessor di kelas CameraXLivePreviewActivity dan berlangganan untuk menerima status emosi. Kemudian kami mengubah probabilitas menjadi flag boolean. Untuk pengujian, mari tambahkan TextView ke layout, di mana kita akan menampilkan emosi melalui emotikon.







Perbedaan langkah penuh.



Bagilah dan mainkan



Karena kami membuat game, kami membutuhkan tempat untuk menggambar elemen. Mari kita asumsikan bahwa ini berjalan di telepon dalam mode potret. Jadi, mari bagi layar menjadi dua bagian: kamera di atas dan game di bawah.



Mengontrol karakter dengan senyuman itu sulit, dan selain itu, hanya ada sedikit waktu di hackathon untuk menerapkan mekanisme tingkat lanjut. Oleh karena itu, karakter kita akan mengumpulkan nishtyaks di jalan, baik di atas lapangan bermain, atau di bawah. Kami akan menambahkan tindakan dengan mata tertutup atau terbuka sebagai komplikasi dari permainan: jika Anda menangkap nishtyak dengan mata tertutup, poinnya menjadi dua kali lipat ( atau setengah dari layar tidak terlihat dan Anda dapat merampok sapi ).



Jika Anda ingin menerapkan gameplay yang berbeda, maka saya dapat menyarankan beberapa opsi menarik:



  • Guitar Hero / Just Dance - analog, di mana Anda perlu menunjukkan emosi tertentu pada musik;
  • perlombaan dengan mengatasi rintangan, di mana Anda harus mencapai garis finish dalam waktu tertentu atau tanpa menabrak;
  • penembak di mana pemain mengedipkan mata dan menembak musuh.


Kami akan menampilkan game dalam Tampilan Android khusus - di sana, dalam metode onDraw, kami akan menggambar karakter di Kanvas. Dalam prototipe pertama, kami akan membatasi diri pada primitif geometris.



Pemain







Karakter kita adalah persegi. Selama inisialisasi, kami akan menyetel ukuran dan posisinya ke kiri, karena akan ada di tempatnya. Posisi sumbu Y akan bergantung pada senyuman pemain. Semua nilai absolut akan dihitung relatif terhadap ukuran area permainan. Ini lebih mudah daripada memilih ukuran tertentu - dan pada perangkat baru kami akan mendapatkan tampilan yang dapat diterima.



private var playerSize = 0
private var playerRect = RectF()
//       View
private fun initializePlayer() {
    playerSize = height / 4
    playerRect.left = playerSize / 2f
    playerRect.right = playerRect.left + playerSize
}
//      
private var flags: EmotionFlags
//      
private fun movePlayer() {
    playerRect.top = getObjectYTopForLine(playerSize, isTopLine = flags.isSmile).toFloat()
    playerRect.bottom = playerRect.top + playerSize
}
//   top     size,
//        
private fun getObjectYTopForLine(size: Int, isTopLine: Boolean): Int {
    return if (isTopLine) {
        width / 2 - width / 4 - size / 2
    } else {
        width / 2 + width / 4 - size / 2
    }
}
//  paint   ,        
private val playerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    style = Paint.Style.FILL
    color = Color.BLUE
}
//     Canvas
private fun drawPlayer(canvas: Canvas) {
    canvas.drawRect(playerRect, playerPaint)
}


kue



Karakter kita "berlari" dan mencoba menangkap kue untuk mencetak poin sebanyak mungkin. Kami menggunakan teknik standar dengan transisi ke sistem referensi relatif terhadap pemain: dia akan berdiri diam, dan kue akan terbang ke arahnya. Jika kuadrat kue berpotongan dengan kuadrat pemain, maka poinnya dihitung. Dan jika pada saat yang sama setidaknya satu mata pengguna ditutup - dua poin Β― \ _ (ツ) _ / Β―



Juga di alam semesta kita hanya akan ada satu kue elektron . Begitu karakter memakannya, ia berpindah dari layar ke strip acak dengan koordinat acak. Ini akan mencegah senyum pemain beresonansi dengan tampilan kue yang bisa diprediksi.



//        
private fun initializeCake() {
    cakeSize = height / 8
    moveCakeToStartPoint()
}
private fun moveCakeToStartPoint() {
    //      
    cakeRect.left = width + width * Random.nextFloat()
    cakeRect.right = cakeRect.left + cakeSize
    //      
    val isTopLine = Random.nextBoolean()
    cakeRect.top = getObjectYTopForLine(cakeSize, isTopLine).toFloat()
    cakeRect.bottom = cakeRect.top + cakeSize
}
//        
private fun moveCake() {
    val currentTime = System.currentTimeMillis()
    val deltaTime = currentTime - previousTimestamp
    val deltaX = cakeSpeed * width * deltaTime
    cakeRect.left -= deltaX
    cakeRect.right = cakeRect.left + cakeSize
    previousTimestamp = currentTime
}
//     ,   
private fun checkPlayerCaughtCake() {
    if (RectF.intersects(playerRect, cakeRect)) {
        score += if (flags.isLeftEyeOpen && flags.isRightEyeOpen) 1 else 2
        moveCakeToStartPoint()
    }
}
//    ,      
private fun checkCakeIsOutOfScreenStart() {
    if (cakeRect.right < 0) {
        moveCakeToStartPoint()
    }
}


Apa yang terjadi



Mari buat tampilan poin menjadi sangat sederhana. Kami akan menampilkan nomor di tengah layar. Anda hanya perlu memperhitungkan tinggi teks dan menjorok ke atas untuk kecantikan.



private val scorePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    color = Color.GREEN
    textSize = context.resources.getDimension(R.dimen.score_size)
}
private var score: Int = 0
private var scorePoint = PointF()
private fun initializeScore() {
    val bounds = Rect()
    scorePaint.getTextBounds("0", 0, 1, bounds)
    val scoreMargin = resources.getDimension(R.dimen.score_margin)
    scorePoint = PointF(width / 2f, scoreMargin + bounds.height())
    score = 0
}


Mari kita lihat mainan apa yang kita buat:





Perbedaan langkah penuh.



Grafonium



Agar tidak malu menampilkan game pada presentasi hackathon, yuk tambahkan sedikit grafonium!







Gambar-gambar



Kami melanjutkan dari fakta bahwa kami tidak dapat menggambar grafik yang mengesankan. Untungnya, ada situs dengan aset permainan gratis. Saya menyukai yang ini , meskipun sekarang tidak tersedia secara langsung karena alasan yang tidak saya ketahui.







Animasi



Kami menggambar di kanvas, yang berarti kami perlu menerapkan animasinya sendiri. Jika ada gambar dengan animasi maka akan mudah untuk diprogram. Kami memperkenalkan kelas untuk objek dengan mengubah gambar.



class AnimatedGameObject(
        private val bitmaps: List<Bitmap>,
        private val duration: Long
) {
    fun getBitmap(timeInMillis: Long): Bitmap {
        val mod = timeInMillis % duration
        val index = (mod / duration.toFloat()) * bitmaps.size
        return bitmaps[index.toInt()]
    }
}


Untuk mendapatkan efek gerakan, latar belakang juga harus dianimasikan. Memiliki serangkaian bingkai latar belakang dalam memori adalah cerita yang terus berlanjut. Oleh karena itu, mari kita lakukan dengan lebih licik: kita akan menggambar satu gambar dengan pergeseran waktu. Garis besar ide:







Selesaikan perbedaan langkah.



Hasil akhir



Sulit untuk menyebutnya mahakarya, tapi tidak masalah untuk prototipe di malam hari. Kode dapat ditemukan di sini . Berjalan secara lokal tanpa kejahatan tambahan.





Sebagai kesimpulan, saya akan menambahkan bahwa Deteksi Wajah ML Kit dapat berguna untuk skenario lain.



Misalnya, untuk mengambil foto narsis yang sempurna dengan teman: Anda dapat menganalisis semua orang dalam bingkai dan memastikan bahwa semua orang tersenyum dan membuka mata. Mendeteksi banyak wajah dalam aliran video berhasil di luar kotak, jadi tugasnya tidak sulit.



Dengan menggunakan pengenalan kontur wajah dari modul Deteksi Wajah, dimungkinkan untuk mereplikasi topeng yang sekarang populer di hampir semua aplikasi kamera. Dan jika Anda menambahkan interaktivitas - melalui definisi senyuman dan kedipan - maka menggunakannya akan sangat menyenangkan.



Fungsi ini - pembentukan wajah - dapat digunakan lebih dari sekadar hiburan. Mereka yang telah mencoba memotong foto untuk dokumen sendiri akan menghargainya. Kami mengambil kontur wajah, secara otomatis memotong foto dengan rasio aspek yang diinginkan dan posisi kepala yang benar. Sensor giroskop akan membantu menentukan sudut pengambilan gambar yang benar.



All Articles