スマイルコントロールゲームを作る

こんにちは!私の名前はIvanShafranです。最近、Android開発者としてVKビデオチームに参加しました。製品アプリケーションとSDKの両方の作成に参加しています。時々、クレイジーなアイデアを実装できるハッカソンに行きます。今日は、数時間で珍しいコントロールを備えたモバイルゲームのプロトタイプを作成する方法を説明します。キャラクターは笑顔とウインクに反応します。







アイデアはどのようにして生まれたのですか



そのようなゲームを作成するというアイデアは、ハッカソンの最中に思いついたものです。このフォーマットでは、開発に1営業日、つまり8時間かかると想定しています。時間内にプロトタイプを作成するために、AndroidSDKを選択しました。ゲームエンジンの方が適しているかもしれませんが、私にはわかりません。



感情の助けを借りて制御するという概念は、別のゲームによって提案されました。そこでは、キャラクターの動きは、声の大きさを変えることによって設定できます。たぶん誰かがすでにゲームコントロールで感情を使っています。しかし、そのような例はほとんど知らないので、この形式に決めました。



大音量のビデオに注意してください!




開発環境のセットアップ



コンピューターに 必要なのはAndroidStudioだけです実行する実際のAndroidデバイスがない場合はWebサイトが有効になっているエミュレーター使用できます



MLキットでプロジェクトを作成する







ML Kitは、ハッカソンの審査員を感動させる優れたツールです。プロトタイプでAIを使用しています。一般に、機械学習に基づくソリューションをプロジェクトに組み込むのに役立ちます。たとえば、フレーム内のオブジェクトを識別する機能、翻訳、テキスト認識などです。



ML Kitには、笑顔や開いた目または閉じた目を認識するための無料のオフラインAPIがあることが重要です。



以前は、ML Kitを使用してプロジェクトを作成するには、最初にFirebaseコンソールに登録する必要がありましたこのステップは、オフライン機能のためにスキップできるようになりました。



Androidアプリ



不要なものを削除



カメラを最初から操作するためのロジックを記述しないために、公式サンプル取得して、不要なものを削除しましょう







まず、ダウンロードして実行してみてください。顔検出モードを調べます。記事のプレビューのように見えます。



マニフェスト



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;
  • 行198〜199のonCreateメソッドの最後で、設定を非表示にします

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


感情の分類を有効にするには、CameraXLivePreviewActivityクラスでFaceDetectorProcessorを設定し、サブスクライブして感情の状態を受け取ります。次に、確率をブールフラグに変換します。テストのために、レイアウトにTextViewを追加してみましょう。ここでは、エモティコンを介して感情を表示します。







フルステップ差。



分割してプレイ



ゲームを作っているので、要素を描く場所が必要です。ポートレートモードの電話で実行されると仮定しましょう。それでは、画面を2つの部分に分けましょう。上部のカメラと下部のゲームです。



笑顔でキャラクターをコントロールすることは難しく、ハッカソンでは高度なメカニズムを実装する時間がほとんどありません。したがって、私たちのキャラクターは、競技場の上部または下部のいずれかにいる途中で、ニッシュタックを収集します。ゲームの複雑さとして、目を閉じた状態または開いた状態のアクションを追加します。目を閉じた状態でニシチャクを捕まえると、ポイントが2倍になります(または画面の半分が表示されず、牛を奪うことができます)。



別のゲームプレイを実装したい場合は、いくつかの興味深いオプションを提案できます。



  • Guitar Hero / JustDance-アナログ。音楽に特定の感情を示す必要があります。
  • 障害を乗り越えて、特定の時間内に、またはクラッシュせずにフィニッシュラインに到達する必要があるレース。
  • プレイヤーがウィンクして敵を撃つシューター。


ゲームをカスタムAndroidビューで表示します。そこで、onDrawメソッドで、Canvasにキャラクターを描画します。最初のプロトタイプでは、幾何学的なプリミティブに制限します。



プレーヤー







私たちのキャラクターは正方形です。初期化中に、サイズと位置が所定の位置にあるため、左側に設定します。Y軸の位置はプレイヤーの笑顔に依存します。すべての絶対値は、ゲームエリアのサイズに関連して計算されます。特定のサイズを選択するよりも簡単です。新しいデバイスで許容できる外観が得られます。



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


ケーキ



私たちのキャラクターは「走って」、できるだけ多くのポイントを獲得するためにケーキを捕まえようとします。標準的な手法を使用して、プレーヤーに関連する参照システムに移行します。プレーヤーは静止し、ケーキは彼に向かって飛んでいきます。ケーキの正方形がプレーヤーの正方形と交差する場合、ポイントがカウントされます。そして、同時にユーザーの少なくとも片方の目を閉じた場合-2つのポイント¯\ _(ツ)_ /¯



また、私たちの宇宙では、電子ケーキは1つだけになりますキャラクターがそれを食べるとすぐに、画面からランダムな座標のランダムなストリップに移動します。これにより、プレーヤーの笑顔が予測可能なケーキの外観に共鳴するのを防ぐことができます。



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


モーションの効果を得るには、背景もアニメーション化する必要があります。一連の背景フレームをメモリに保持することは、オーバーヘッドストーリーです。したがって、もっと巧妙にやってみましょう。タイムシフトを使用して1つの画像を描画します。アイデアの概要:







完全なステップ差。



最終結果



傑作とは言い難いですが、夕方の試作品としては問題ありません。コードはここにあります追加のシェナニガンなしでローカルに実行されます。





結論として、MLキットの顔検出は他のシナリオにも役立つ可能性があることを付け加えておきます。



たとえば、友達と完璧なセルフを撮るには、フレーム内のすべての人を分析し、全員が笑顔で目を開いていることを確認できます。ビデオストリーム内の複数の顔を検出することは、箱から出してすぐに機能するため、タスクは難しくありません。



顔検出モジュールの顔の輪郭認識を使用すると、ほとんどすべてのカメラアプリケーションで現在普及しているマスクを複製することができます。そして、笑顔とウィンクの定義を通じてインタラクティブ性を追加すると、それらを使用することは2倍楽しいでしょう。



この機能(顔の輪郭)は、娯楽以外にも使用できます。自分でドキュメント用の写真を切り抜こうとした人は喜ぶでしょう。顔の輪郭を取り、希望のアスペクト比と正しい頭の位置で写真を自動的に切り取ります。ジャイロスコープセンサーは、正しい撮影角度を決定するのに役立ちます。



All Articles