Ein Lächeln-Kontrollspiel machen

Hallo! Mein Name ist Ivan Shafran, ich bin kürzlich als Android-Entwickler dem VK-Videoteam beigetreten. Ich bin an der Erstellung von Produktanwendungen und SDKs beteiligt. Von Zeit zu Zeit besuche ich Hackathons, bei denen Sie verrückte Ideen umsetzen können. Heute werde ich Ihnen sagen, wie Sie in ein paar Stunden einen Prototyp eines Handyspiels mit ungewöhnlichen Steuerelementen erstellen können: Ein Charakter reagiert auf ein Lächeln und ein Augenzwinkern.







Wie ist die Idee entstanden?



Die Idee, ein solches Spiel zu entwickeln, kam direkt während des Hackathons. Das Format ging davon aus, dass es einen Arbeitstag für die Entwicklung gab, dh 8 Stunden. Um rechtzeitig einen Prototyp zu erstellen, habe ich mich für das Android SDK entschieden. Vielleicht wären Game Engines besser geeignet, aber ich verstehe sie nicht.



Das Konzept der Steuerung mit Hilfe von Emotionen wurde von einem anderen Spiel vorgeschlagen: Dort können die Bewegungen des Charakters durch Ändern der Lautstärke Ihrer Stimme eingestellt werden. Vielleicht hat jemand bereits Emotionen in der Spielsteuerung eingesetzt. Aber ich kenne nur wenige solcher Beispiele, deshalb habe ich mich für dieses Format entschieden.



Vorsicht vor lauten Videos!




Einrichten der Entwicklungsumgebung



Wir brauchen nur Android Studio auf dem Computer. Wenn kein echtes Android- Gerät ausgeführt werden kann, können Sie einen Emulator mit aktivierter Webcam verwenden .



Erstellen Sie ein Projekt mit ML Kit







ML Kit ist ein großartiges Werkzeug, um die Hackathon-Jury zu beeindrucken: Sie verwenden KI in einem Prototyp! Im Allgemeinen hilft es, Lösungen, die auf maschinellem Lernen basieren, in Projekte einzubetten, z. B. Funktionen zum Bestimmen von Objekten in einem Rahmen, Übersetzung und Texterkennung.



Für uns ist es wichtig, dass ML Kit über eine kostenlose Offline-API zum Erkennen von Lächeln und offenen oder geschlossenen Augen verfügt.



Zuvor mussten Sie sich zunächst in der Firebase-Konsole registrieren, um ein Projekt mit ML Kit zu erstellen . Dieser Schritt kann jetzt für die Offline-Funktionalität übersprungen werden.



Android App



Unnötiges entfernen



Um die Logik für die Arbeit mit der Kamera nicht von Grund auf neu zu schreiben, nehmen wir das offizielle Beispiel und entfernen das, was wir nicht benötigen.







Laden Sie zuerst das Beispiel herunter und versuchen Sie es auszuführen. Entdecken Sie den Gesichtserkennungsmodus: Er sieht aus wie die Artikelvorschau.



Manifest



Beginnen wir mit der Bearbeitung von AndroidManifest.xml. Entfernen Sie alle Aktivitäts-Tags mit Ausnahme des ersten. Und an seine Stelle setzen wir CameraXLivePreviewActivity, um sofort von der Kamera aus zu starten. Im Wert des Attributs android: value lassen wir nur Gesicht, um unnötige Ressourcen von der APK auszuschließen.



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


Vollschrittdiff.



Kamera



Sparen wir Zeit - wir löschen keine unnötigen Dateien, sondern konzentrieren uns auf die Elemente des CameraXLivePreviewActivity-Bildschirms.



  • Stellen Sie in Zeile 117 den Gesichtserkennungsmodus ein:

    private String selectedModel = FACE_DETECTION;
  • Schalten Sie in Zeile 118 die Frontkamera ein:

    private int lensFacing = CameraSelector.LENS_FACING_FRONT;
  • Blenden Sie am Ende der onCreate-Methode in den Zeilen 198-199 die Einstellungen aus

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


Wir können hier aufhören. Wenn FPS-Rendering und Gesichtsraster jedoch visuell ablenken, können Sie sie folgendermaßen deaktivieren:



  • Löschen Sie in der Datei VisionProcessorBase.java die Zeilen 213-215, um die FPS auszublenden:

    graphicOverlay.add(
           new InferenceInfoGraphic(
              graphicOverlay, currentLatencyMs, shouldShowFps ? framesPerSecond : null));
  • Löschen Sie in der Datei FaceDetectorProcessor.java die Zeilen 75–78, um das Gesichtsnetz auszublenden:

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


Vollschrittdiff.



Emotionen erkennen



Die Lächelnerkennung ist standardmäßig deaktiviert, der Einstieg ist jedoch einfach. Nicht umsonst haben wir den Beispielcode zugrunde gelegt! Lassen Sie uns die benötigten Parameter in einer separaten Klasse auswählen und die Listener-Schnittstelle deklarieren:



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


Um die Emotionsklassifizierung zu aktivieren, richten Sie den FaceDetectorProcessor in der CameraXLivePreviewActivity-Klasse ein und abonnieren Sie, um den Emotionsstatus zu erhalten. Dann transformieren wir die Wahrscheinlichkeiten in boolesche Flags. Fügen Sie zum Testen dem Layout eine Textansicht hinzu, in der wir Emotionen durch Emoticons anzeigen.







Vollschrittdiff.



Teilen und spielen



Da wir ein Spiel machen, brauchen wir einen Ort, an dem wir die Elemente zeichnen können. Nehmen wir an, dass es auf dem Telefon im Hochformat ausgeführt wird. Teilen wir den Bildschirm also in zwei Teile: die Kamera oben und das Spiel unten.



Es ist schwierig, einen Charakter mit einem Lächeln zu kontrollieren, und außerdem bleibt beim Hackathon wenig Zeit, um fortschrittliche Mechanik zu implementieren. Daher wird unser Charakter auf dem Weg Nishtyaks sammeln, entweder oben auf dem Spielfeld oder unten. Wir werden Aktionen mit geschlossenen oder offenen Augen als Komplikation des Spiels hinzufügen: Wenn Sie einen Nishtyak mit einem geschlossenen Auge fangen, werden die Punkte verdoppelt ( oder die Hälfte des Bildschirms ist nicht sichtbar und Sie können die Kühe ausrauben ).



Wenn Sie ein anderes Gameplay implementieren möchten, kann ich einige interessante Optionen vorschlagen:



  • Guitar Hero / Just Dance - analog, wo Sie der Musik eine bestimmte Emotion zeigen müssen;
  • ein Rennen mit Überwindung von Hindernissen, bei dem Sie in einer bestimmten Zeit oder ohne Sturz die Ziellinie erreichen müssen;
  • Schütze, wo der Spieler zwinkert und den Feind erschießt.


Wir werden das Spiel in einer benutzerdefinierten Android-Ansicht anzeigen - dort werden wir in der onDraw-Methode einen Charakter auf Canvas zeichnen. Im ersten Prototyp beschränken wir uns auf geometrische Grundelemente.



Spieler







Unser Charakter ist ein Quadrat. Während der Initialisierung werden Größe und Position links eingestellt, da sie vorhanden sind. Die Position der Y-Achse hängt vom Lächeln des Spielers ab. Alle absoluten Werte werden relativ zur Größe des Spielbereichs berechnet. Es ist einfacher als die Auswahl bestimmter Größen - und wir erhalten ein akzeptables Aussehen für neue Geräte.



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


Kuchen



Unser Charakter "rennt" und versucht Kuchen zu fangen, um so viele Punkte wie möglich zu erzielen. Wir verwenden die Standardtechnik beim Übergang zum Referenzsystem relativ zum Spieler: Er wird still stehen und die Kuchen werden auf ihn zufliegen. Wenn sich das Quadrat des Kuchens mit dem Quadrat des Spielers schneidet, wird der Punkt gezählt. Und wenn gleichzeitig mindestens ein Auge des Benutzers geschlossen ist - zwei Punkte ¯ \ _ (ツ) _ / ¯



Auch in unserem Universum wird es nur einen Elektronenkuchen geben . Sobald der Charakter es isst, bewegt er sich vom Bildschirm zu einem zufälligen Streifen mit einer zufälligen Koordinate. Dies verhindert, dass das Lächeln des Spielers mit dem vorhersehbaren Aussehen des Kuchens übereinstimmt.



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


Was ist passiert



Machen wir die Anzeige von Punkten sehr einfach. Wir werden die Nummer in der Mitte des Bildschirms anzeigen. Sie müssen nur die Höhe des Textes berücksichtigen und die Oberseite für Schönheit einrücken.



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
}


Mal sehen, was für ein Spielzeug wir gemacht haben:





Vollschrittdiff.



Graphonium



Um es nicht zu schämen, das Spiel bei der Präsentation des Hackathons zu zeigen, fügen wir ein wenig Grafonium hinzu!







Bilder



Wir gehen davon aus, dass wir keine beeindruckenden Grafiken zeichnen können. Glücklicherweise gibt es Websites mit kostenlosen Gaming-Assets. Ich mochte dieses , obwohl es jetzt aus einem mir unbekannten Grund nicht direkt verfügbar ist.







Animation



Wir greifen auf Canvas zurück, was bedeutet, dass wir die Animation selbst implementieren müssen. Wenn es Bilder mit Animation gibt, ist es einfach zu programmieren. Wir führen eine Klasse für ein Objekt mit sich ändernden Bildern ein.



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


Um den Bewegungseffekt zu erzielen, muss auch der Hintergrund animiert werden. Eine Reihe von Hintergrundbildern im Speicher zu haben, ist eine Overhead-Geschichte. Machen wir es deshalb schlauer: Wir zeichnen ein Bild mit einer Zeitverschiebung. Ideenübersicht:







Vollständiger Schrittunterschied.



Endergebnis



Es ist schwer, es als Meisterwerk zu bezeichnen, aber für einen Prototyp über Nacht ist es in Ordnung. Den Code finden Sie hier . Läuft lokal ohne zusätzliche Spielereien.





Abschließend möchte ich hinzufügen, dass die ML Kit-Gesichtserkennung für andere Szenarien nützlich sein kann.



Zum Beispiel, um perfekte Selfies mit Freunden zu machen: Sie können alle Personen im Rahmen analysieren und sicherstellen, dass alle lächelten und die Augen öffneten. Das Erkennen mehrerer Gesichter in einem Videostream funktioniert sofort, sodass die Aufgabe nicht schwierig ist.



Mithilfe der Gesichtskonturerkennung aus dem Gesichtserkennungsmodul können Masken repliziert werden, die heute in fast allen Kameraanwendungen beliebt sind. Und wenn Sie interaktiv hinzufügen - durch die Definition eines Lächelns und eines Augenzwinkerns - dann macht die Verwendung doppelt Spaß.



Diese Funktionalität - Gesichtskonturierung - kann nicht nur zur Unterhaltung verwendet werden. Diejenigen, die versucht haben, ein Foto für Dokumente selbst auszuschneiden, werden es zu schätzen wissen. Wir nehmen die Gesichtskontur, schneiden das Foto automatisch mit dem gewünschten Seitenverhältnis und der richtigen Kopfposition aus. Der Gyroskopsensor hilft bei der Bestimmung des richtigen Aufnahmewinkels.



All Articles