Hacer un juego de control de sonrisas

¡Hola! Mi nombre es Ivan Shafran, recientemente me uní al equipo de video de VK como desarrollador de Android. Participo en la creación tanto de aplicaciones de producto como de SDK. De vez en cuando visito hackathons donde puedes implementar cualquier idea loca. Hoy te cuento cómo hacer un prototipo de juego para móvil con controles inusuales en un par de horas: un personaje reaccionará ante una sonrisa y un guiño.







¿Cómo surgió la idea?



La idea de crear un juego así surgió durante el hackathon. El formato asumió que había un día laborable para el desarrollo, es decir, 8 horas. Para hacer un prototipo a tiempo, elegí el SDK de Android. Quizás los motores de juegos serían más adecuados, pero no los entiendo.



Otro juego sugirió el concepto de controlar con la ayuda de las emociones: allí, los movimientos del personaje se podían configurar cambiando el volumen de tu voz. Quizás alguien ya haya usado las emociones en el control del juego. Pero conozco pocos ejemplos de este tipo, así que me decidí por este formato.



¡Cuidado con los videos ruidosos!




Configurar el entorno de desarrollo



Solo necesitamos Android Studio en la computadora. Si no hay un dispositivo Android real para ejecutar, puede usar un emulador con una cámara web habilitada .



Crea un proyecto con ML Kit







ML Kit es una gran herramienta para impresionar al jurado del hackathon: ¡estás usando IA en un prototipo! En general, ayuda a integrar soluciones basadas en el aprendizaje automático en proyectos, por ejemplo, funcionalidad para definir objetos en un marco, traducción y reconocimiento de texto.



Para nosotros es importante que ML Kit tenga una API sin conexión gratuita para reconocer sonrisas y ojos abiertos o cerrados.



Anteriormente, para crear cualquier proyecto con ML Kit, primero tenía que registrarse en Firebase console . Este paso ahora se puede omitir para la funcionalidad sin conexión.



aplicación Android



Eliminar innecesario



Para no escribir lógica para trabajar con la cámara desde cero, tomemos la muestra oficial y eliminemos de ella lo que no necesitamos.







Primero, descargue el ejemplo e intente ejecutarlo. Explore el modo de detección de rostros: se verá como la vista previa del artículo.



Manifiesto



Comencemos a editar AndroidManifest.xml. Elimine todas las etiquetas de actividad excepto la primera. Y en su lugar pondremos CameraXLivePreviewActivity para que se inicie inmediatamente desde la cámara. En el valor del atributo android: value, dejamos solo la cara para excluir recursos innecesarios del 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>


Diferencia de paso completo.



Cámara



Ahorremos tiempo: no eliminaremos archivos innecesarios, sino que nos centraremos en los elementos de la pantalla CameraXLivePreviewActivity.



  • En la línea 117, configure el modo de detección de rostros:

    private String selectedModel = FACE_DETECTION;
  • En la línea 118, encienda la cámara frontal:

    private int lensFacing = CameraSelector.LENS_FACING_FRONT;
  • Al final del método onCreate en las líneas 198-199, oculte la configuración

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


Podemos detenernos aquí. Pero si el renderizado de FPS y la cuadrícula de rostros distraen visualmente, puede desactivarlos así:



  • En el archivo VisionProcessorBase.java, elimine las líneas 213-215 para ocultar el FPS:

    graphicOverlay.add(
           new InferenceInfoGraphic(
              graphicOverlay, currentLatencyMs, shouldShowFps ? framesPerSecond : null));
  • En el archivo FaceDetectorProcessor.java, elimine las líneas 75–78 para ocultar la malla de la cara:

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


Diferencia de paso completo.



Reconociendo emociones



La detección de sonrisas está desactivada de forma predeterminada, pero es fácil comenzar. ¡No es por nada que tomamos el código de ejemplo como base! Seleccionemos los parámetros que necesitamos en una clase separada y declaremos la interfaz de escucha:



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


Para habilitar la clasificación de emociones, configure FaceDetectorProcessor en la clase CameraXLivePreviewActivity y suscríbase para recibir el estado de emoción. Luego transformamos las probabilidades en banderas booleanas. Para probar, agreguemos un TextView al diseño, en el que mostraremos emociones a través de emoticonos.







Diferencia de paso completo.



Divide y juega



Como estamos haciendo un juego, necesitamos un lugar para dibujar los elementos. Supongamos que se ejecuta en el teléfono en modo vertical. Entonces, dividamos la pantalla en dos partes: la cámara en la parte superior y el juego en la parte inferior.



Controlar a un personaje con una sonrisa es difícil y, además, hay poco tiempo en el hackathon para implementar mecánicas avanzadas. Por lo tanto, nuestro personaje recolectará nishtyaks a lo largo del camino, ya sea en la parte superior del campo de juego o en la parte inferior. Agregaremos acciones con los ojos cerrados o abiertos como complicación del juego: si atrapas un nishtyak con los ojos cerrados, los puntos se duplican ( o la mitad de la pantalla no es visible y puedes robar las vacas ).



Si desea implementar un juego diferente, puedo sugerir algunas opciones interesantes:



  • Guitar Hero / Just Dance - analógico, donde necesitas mostrar cierta emoción a la música;
  • una carrera con superación de obstáculos, donde debes llegar a la meta en un tiempo determinado o sin chocar;
  • tirador donde el jugador guiña un ojo y dispara al enemigo.


Mostraremos el juego en una vista de Android personalizada; allí, en el método onDraw, dibujaremos un personaje en Canvas. En el primer prototipo, nos limitaremos a las primitivas geométricas.



Jugador







Nuestro personaje es un cuadrado. Durante la inicialización, estableceremos su tamaño y posición a la izquierda, ya que estará en su lugar. La posición del eje Y dependerá de la sonrisa del jugador. Todos los valores absolutos se calcularán en relación con el tamaño del área de juego. Es más fácil que elegir tamaños específicos, y obtendremos un aspecto aceptable en dispositivos nuevos.



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


pastel



Nuestro personaje "corre" e intenta atrapar pasteles para sumar tantos puntos como sea posible. Usamos la técnica estándar con la transición al sistema de referencia relativo al jugador: se quedará quieto y los pasteles volarán hacia él. Si el cuadrado del pastel se cruza con el cuadrado del jugador, se cuenta el punto. Y si al mismo tiempo se cierra al menos un ojo del usuario - dos puntos ¯ \ _ (ツ) _ / ¯



También en nuestro universo habrá sólo una torta de electrones . Tan pronto como el personaje lo come, sale de la pantalla a una franja aleatoria con una coordenada aleatoria. Esto evitará que la sonrisa del jugador resuene con la apariencia predecible del pastel.



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


Que pasó



Hagamos la visualización de puntos muy simple. Mostraremos el número en el centro de la pantalla. Solo necesita tener en cuenta la altura del texto y sangrar la parte superior para darle belleza.



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
}


Veamos qué tipo de juguete hicimos:





Diferencia de paso completo.



Grafonio



Para que no se avergüence de mostrar el juego en la presentación del hackathon, ¡agreguemos un poco de grafonio!







Imagenes



Partimos del hecho de que no podemos dibujar gráficos impresionantes. Afortunadamente, hay sitios con activos de juegos gratuitos. Me gustó este , aunque ahora no está disponible directamente por una razón que desconozco.







Animación



Dibujamos en Canvas, lo que significa que debemos implementar la animación nosotros mismos. Si hay imágenes con animación, será fácil de programar. Introducimos una clase para un objeto con imágenes cambiantes.



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


Para obtener el efecto de movimiento, el fondo también debe estar animado. Tener una serie de marcos de fondo en la memoria es una historia general. Por lo tanto, hagámoslo con más astucia: dibujaremos una imagen con un cambio de tiempo. Esquema de la idea:







Complete el paso diff.



Resultado final



Es difícil llamarlo una obra maestra, pero está bien para un prototipo por la noche. El código se puede encontrar aquí . Funciona localmente sin travesuras adicionales.





En conclusión, agregaré que ML Kit Face Detection puede ser útil para otros escenarios.



Por ejemplo, para tomar selfies perfectos con amigos: puedes analizar a todas las personas en el encuadre y asegurarte de que todos sonrieron y abrieron los ojos. La detección de múltiples caras en una secuencia de video funciona de manera inmediata, por lo que la tarea no es difícil.



Utilizando el reconocimiento de contorno facial del módulo de Detección facial, es posible replicar máscaras que ahora son populares en casi todas las aplicaciones de la cámara. Y si agrega interactivos, a través de la definición de una sonrisa y un guiño, entonces usarlos será doblemente divertido.



Esta funcionalidad, el contorno facial, se puede utilizar para algo más que entretenimiento. Aquellos que hayan intentado recortar una foto para documentos ellos mismos lo apreciarán. Tomamos el contorno de la cara, recortamos automáticamente la foto con la relación de aspecto deseada y la posición correcta de la cabeza. El sensor del giroscopio ayudará a determinar el ángulo de disparo correcto.



All Articles