صنع لعبة التحكم في الابتسامة

مرحبا! اسمي إيفان شافران ، انضممت مؤخرًا إلى فريق فيديو VK كمبرمج مطور لنظام Android. أشارك في إنشاء كل من تطبيقات المنتجات ومجموعات تطوير البرامج. من وقت لآخر أزور هاكاثون حيث يمكنك تنفيذ أي أفكار مجنونة. سأخبرك اليوم كيف تصنع نموذجًا أوليًا للعبة محمولة بعناصر تحكم غير عادية في غضون ساعتين: ستتفاعل الشخصية مع ابتسامة وغمزة.







كيف خطرت لك الفكرة



جاءت فكرة إنشاء مثل هذه اللعبة أثناء الهاكاثون. افترض التنسيق أنه كان هناك يوم عمل واحد للتطوير ، أي 8 ساعات. لعمل نموذج أولي في الوقت المناسب ، اخترت Android SDK. ربما تكون محركات الألعاب مناسبة بشكل أفضل ، لكنني لا أفهمها.



تم اقتراح مفهوم التحكم بمساعدة العواطف من قبل لعبة أخرى: هناك ، يمكن ضبط حركات الشخصية عن طريق تغيير مستوى صوتك. ربما استخدم شخص ما المشاعر بالفعل في التحكم في اللعبة. لكني أعرف القليل من هذه الأمثلة ، لذلك استقرت على هذا الشكل.



احذر من الفيديو بصوت عال!




تهيئة البيئة التنموية



نحتاج فقط إلى Android Studio على الكمبيوتر. إذا لم يكن هناك جهاز Android حقيقي لتشغيله ، فيمكنك استخدام محاكي مع تمكين كاميرا الويب .



أنشئ مشروعًا باستخدام ML Kit







ML Kit هي أداة رائعة لإقناع لجنة تحكيم الهاكاثون: أنت تستخدم الذكاء الاصطناعي في نموذج أولي! بشكل عام ، من المفيد تضمين الحلول القائمة على التعلم الآلي في المشاريع ، على سبيل المثال ، وظيفة تحديد الكائنات في إطار والترجمة والتعرف على النص.



من المهم بالنسبة لنا أن يكون لدى ML Kit واجهة برمجة تطبيقات مجانية غير متصلة بالإنترنت للتعرف على الابتسامات والعينين المفتوحة أو المغلقة.



في السابق ، لإنشاء أي مشروع باستخدام ML Kit ، كان عليك أولاً التسجيل في وحدة تحكم Firebase . يمكن الآن تخطي هذه الخطوة للوظائف غير المتصلة.



الروبوت التطبيق



إزالة لا لزوم لها



حتى لا نكتب منطق العمل بالكاميرا من الصفر ، فلنأخذ العينة الرسمية ونزيل منها ما لا نحتاجه.







أولاً ، قم بتنزيل المثال وحاول التشغيل. استكشف وضع اكتشاف الوجه: سيبدو مثل معاينة المقالة.



بيان



لنبدأ في تحرير AndroidManifest.xml. قم بإزالة جميع علامات النشاط باستثناء العلامة الأولى. وفي مكانه سنضع CameraXLivePreviewActivity ليبدأ على الفور من الكاميرا. في قيمة السمة android: value ، نترك الوجه فقط لاستبعاد الموارد غير الضرورية من 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>


فرق خطوة كاملة.



الة تصوير



دعونا نوفر الوقت - لن نحذف الملفات غير الضرورية ، وبدلاً من ذلك سنركز على عناصر شاشة CameraXLivePreviewActivity.



  • في السطر 117 ، اضبط وضع اكتشاف الوجه:

    private String selectedModel = FACE_DETECTION;
  • على الخط 118 ، قم بتشغيل الكاميرا الأمامية:

    private int lensFacing = CameraSelector.LENS_FACING_FRONT;
  • في نهاية طريقة onCreate في الأسطر 198-199 ، قم بإخفاء الإعدادات

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


يمكننا التوقف هنا. ولكن إذا كان عرض FPS وشبكة الوجه يشتتان بصريًا ، فيمكنك إيقاف تشغيلهما على النحو التالي:



  • في ملف VisionProcessorBase.java ، احذف الأسطر 213-215 لإخفاء FPS:

    graphicOverlay.add(
           new InferenceInfoGraphic(
              graphicOverlay, currentLatencyMs, shouldShowFps ? framesPerSecond : null));
  • في ملف FaceDetectorProcessor.java ، احذف الأسطر 75-78 لإخفاء شبكة الوجه:

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


فرق خطوة كاملة.



التعرف على المشاعر



يتم إيقاف تشغيل اكتشاف الابتسامة افتراضيًا ، ولكن من السهل البدء. ليس من أجل لا شيء أخذنا مثال الكود كأساس! لنحدد المعلمات التي نحتاجها في فئة منفصلة ونعلن عن واجهة المستمع:



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()
                ));
            }
        }
    }
}


لتمكين تصنيف المشاعر ، قم بإعداد FaceDetectorProcessor في فئة CameraXLivePreviewActivity واشترك لتلقي حالة المشاعر. ثم نقوم بتحويل الاحتمالات إلى أعلام منطقية. للاختبار ، دعنا نضيف TextView إلى التخطيط ، حيث سنظهر المشاعر من خلال الرموز.







فرق خطوة كاملة.



فرّق والعب



نظرًا لأننا نصنع لعبة ، نحتاج إلى مكان لرسم العناصر. لنفترض أنه يعمل على الهاتف في الوضع الرأسي. لذا ، دعونا نقسم الشاشة إلى قسمين: الكاميرا في الأعلى واللعبة في الأسفل.



يعد التحكم في شخصية بابتسامة أمرًا صعبًا ، وإلى جانب ذلك ، هناك القليل من الوقت في الهاكاثون لتطبيق آليات متقدمة. لذلك ، ستقوم شخصيتنا بجمع nishtyaks على طول الطريق ، سواء في الجزء العلوي من الملعب أو في الجزء السفلي. سنضيف إجراءات بعيون مغلقة أو مفتوحة كمضاعفات للعبة: إذا قبضت على nishtyak بعين مغلقة ، تتضاعف النقاط ( أو نصف الشاشة غير مرئي ويمكنك سرقة بقرة ).



إذا كنت ترغب في تنفيذ طريقة لعب مختلفة ، فيمكنني اقتراح بعض الخيارات المثيرة للاهتمام:



  • Guitar Hero / Just Dance - تناظرية ، حيث تحتاج إلى إظهار عاطفة معينة للموسيقى ؛
  • سباق مع التغلب على العقبات ، حيث تحتاج إلى الوصول إلى خط النهاية في وقت معين أو دون الاصطدام ؛
  • مطلق النار حيث يغمز اللاعب ويطلق النار على العدو.


سنعرض اللعبة في عرض Android مخصص - هناك ، في طريقة onDraw ، سنرسم شخصية على Canvas. في النموذج الأول ، سنقتصر على البدائيين الهندسيين.



لاعب







شخصيتنا مربع. أثناء التهيئة ، سنضبط حجمه وموضعه على اليسار ، لأنه سيكون في مكانه. يعتمد موضع المحور الصادي على ابتسامة اللاعب. سيتم حساب جميع القيم المطلقة بالنسبة لحجم منطقة اللعبة. إنه أسهل من اختيار أحجام معينة - وسوف نحصل على مظهر مقبول على الأجهزة الجديدة.



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


كيك



شخصيتنا "تجري" وتحاول التقاط الكعك من أجل تسجيل أكبر عدد ممكن من النقاط. نستخدم الأسلوب القياسي مع الانتقال إلى النظام المرجعي المتعلق باللاعب: سيقف ثابتًا ، وستطير الكعك باتجاهه. إذا تقاطع مربع الكعكة مع مربع اللاعب ، يتم حساب النقطة. وإذا تم إغلاق عين واحدة على الأقل للمستخدم في نفس الوقت - نقطتان ¯ \ _ (ツ) _ / ¯



أيضًا في كوننا ستكون هناك كعكة إلكترونية واحدة فقط . بمجرد أن تأكلها الشخصية ، تنتقل من الشاشة إلى شريط عشوائي بتنسيق عشوائي. سيمنع هذا ابتسامة اللاعب من الصدى مع المظهر المتوقع للكعكة.



//        
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()
    }
}


ماذا حدث



لنجعل عرض النقاط بسيطًا جدًا. سوف نعرض الرقم في وسط الشاشة. تحتاج فقط إلى مراعاة ارتفاع النص ووضع مسافة بادئة للأعلى للجمال.



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
}


دعونا نرى نوع اللعبة التي صنعناها:





فرق خطوة كاملة.



الجرافونيوم



حتى لا تخجل من عرض اللعبة في عرض الهاكاثون ، دعنا نضيف بعض الجرافونيوم!







الصور



ننطلق من حقيقة أنه لا يمكننا رسم رسومات رائعة. لحسن الحظ ، هناك مواقع بها أصول ألعاب مجانية. لقد أحببت هذا ، على الرغم من أنه غير متوفر الآن بشكل مباشر لسبب غير معروف بالنسبة لي.







حيوية



نحن نرسم على Canvas ، مما يعني أننا بحاجة إلى تنفيذ الرسوم المتحركة بأنفسنا. إذا كانت هناك صور بها رسوم متحركة ، فسيكون من السهل برمجتها. نقدم فئة لكائن مع تغيير الصور.



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()]
    }
}


للحصول على تأثير الحركة ، يجب أيضًا أن تكون الخلفية متحركة. إن وجود سلسلة من الإطارات الخلفية في الذاكرة هو قصة عامة. لذلك ، دعونا نفعل ذلك بشكل أكثر ذكاءً: سنرسم صورة واحدة مع تحول زمني. مخطط الفكرة:







فرق خطوة كاملة.



النتيجة النهائية



من الصعب أن نسميها تحفة ، لكن لا بأس من أن تصنع نموذجًا أوليًا بين عشية وضحاها. يمكن العثور على الرمز هنا . يتم تشغيله محليًا بدون خدع إضافية.





في الختام ، سأضيف أن تقنية ML Kit Face Detection يمكن أن تكون مفيدة لسيناريوهات أخرى.



على سبيل المثال ، لالتقاط صور سيلفي مثالية مع الأصدقاء: يمكنك تحليل جميع الأشخاص الموجودين في الصورة والتأكد من أن الجميع يبتسم ويفتح أعينهم. يعمل اكتشاف الوجوه المتعددة في دفق الفيديو خارج الصندوق ، وبالتالي فإن المهمة ليست صعبة.



باستخدام التعرف على محيط الوجه من وحدة Face Detection ، من الممكن تكرار الأقنعة التي أصبحت شائعة الآن في جميع تطبيقات الكاميرا تقريبًا. وإذا أضفت تفاعلية - من خلال تعريف الابتسامة والغمزة - فسيكون استخدامها متعة مضاعفة.



يمكن استخدام هذه الوظيفة - تحديد الوجه - لأكثر من الترفيه. أولئك الذين حاولوا قص صورة للمستندات بأنفسهم سيقدرون ذلك. نأخذ محيط الوجه ، ونقطع الصورة تلقائيًا بنسبة العرض إلى الارتفاع المطلوبة وموضع الرأس الصحيح. سيساعد مستشعر الجيروسكوب في تحديد زاوية التصوير الصحيحة.



All Articles