
Como surgiu a ideia
A ideia de criar tal jogo surgiu durante o hackathon. O formato pressupunha que houvesse uma jornada útil para desenvolvimento, ou seja, 8 horas. Para fazer um protótipo a tempo, escolhi o Android SDK. Talvez os motores de jogo fossem mais adequados, mas não os entendo.
O conceito de controlar com a ajuda das emoções foi sugerido por outro jogo: ali, os movimentos do personagem podiam ser ajustados alterando o volume da sua voz. Talvez alguém já tenha usado emoções no controle do jogo. Mas conheço poucos exemplos desse tipo, então escolhi esse formato.
Cuidado com o vídeo alto!
Configurando o ambiente de desenvolvimento
Precisamos apenas do Android Studio no computador. Se não houver um dispositivo Android real para executar, você pode usar um emulador com uma webcam habilitada .
Crie um projeto com o kit de ML

O ML Kit é uma ótima ferramenta para impressionar o júri do hackathon: você está usando IA em um protótipo! Em geral, ajuda a incorporar soluções baseadas em aprendizado de máquina em projetos, por exemplo, funcionalidade para identificação de objetos em um quadro, tradução e reconhecimento de texto.
É importante para nós que o ML Kit tenha uma API off-line gratuita para reconhecer sorrisos e olhos abertos ou fechados.
Anteriormente, para criar qualquer projeto com o ML Kit, primeiro você precisava se registrar no Firebase console . Esta etapa agora pode ser ignorada para a funcionalidade offline.
App Android
Remover desnecessário
Para não escrever lógica para trabalhar com a câmera do zero, vamos pegar a amostra oficial e retirar dela o que não precisamos.

Primeiro, baixe o exemplo e tente executá-lo. Explore o modo de detecção de rosto: será semelhante à prévia do artigo.
Manifesto
Vamos começar a editar AndroidManifest.xml. Remova todas as tags de atividade, exceto a primeira. E em seu lugar colocaremos CameraXLivePreviewActivity para iniciar imediatamente a partir da câmera. No valor do atributo android: value, deixamos apenas o rosto para excluir recursos desnecessários do 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>
Diferença passo completo.
Câmera
Vamos economizar tempo - não excluiremos arquivos desnecessários; em vez disso, vamos nos concentrar nos elementos da tela CameraXLivePreviewActivity.
- Na linha 117, defina o modo de detecção de rosto:
private String selectedModel = FACE_DETECTION;
- Na linha 118, ligue a câmera frontal:
private int lensFacing = CameraSelector.LENS_FACING_FRONT;
- No final do método onCreate nas linhas 198-199, oculte as configurações
findViewById( R.id.settings_button ).setVisibility( View.GONE ); findViewById( R.id.control ).setVisibility( View.GONE );
Podemos parar aqui. Mas se a renderização de FPS e a grade de rosto distraem visualmente, você pode desativá-los desta forma:
- No arquivo VisionProcessorBase.java, exclua as linhas 213-215 para ocultar o FPS:
graphicOverlay.add( new InferenceInfoGraphic( graphicOverlay, currentLatencyMs, shouldShowFps ? framesPerSecond : null));
- No arquivo FaceDetectorProcessor.java, exclua as linhas 75-78 para ocultar a malha do rosto:
for (Face face : faces) { graphicOverlay.add(new FaceGraphic(graphicOverlay, face)); logExtrasForTesting(face); }
Diferença passo completo.
Reconhecendo emoções
A detecção de sorriso está desativada por padrão, mas é muito fácil iniciá-la. Não é à toa que tomamos o código de exemplo como base! Vamos selecionar os parâmetros de que precisamos em uma classe separada e declarar a interface do ouvinte:
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 ativar a classificação de emoção, configure o FaceDetectorProcessor na classe CameraXLivePreviewActivity e inscreva-se para receber o estado de emoção. Em seguida, transformamos as probabilidades em sinalizadores booleanos. Para teste, vamos adicionar um TextView ao layout, no qual mostraremos emoções por meio de emoticons.

Diferença passo completo.
Divida e brinque
Já que estamos fazendo um jogo, precisamos de um lugar para desenhar os elementos. Vamos supor que ele seja executado no telefone no modo retrato. Então, vamos dividir a tela em duas partes: a câmera na parte superior e o jogo na parte inferior.
Controlar um personagem com um sorriso é difícil e, além disso, há pouco tempo no hackathon para implementar mecânicas avançadas. Portanto, nosso personagem irá coletar nishtyaks ao longo do caminho, seja no topo do campo de jogo ou na parte inferior. Adicionaremos ações com os olhos fechados ou abertos como uma complicação do jogo: se você pegar um nishtyak com os olhos fechados, os pontos são duplicados (
Se você deseja implementar uma jogabilidade diferente, posso sugerir algumas opções interessantes:
- Guitar Hero / Just Dance - analógico, onde você precisa mostrar uma certa emoção à música;
- uma corrida com superação de obstáculos, onde é preciso chegar à linha de chegada em um determinado tempo ou sem bater;
- atirador onde o jogador pisca e atira no inimigo.
Iremos exibir o jogo em uma Visualização Android customizada - lá, no método onDraw, desenharemos um personagem no Canvas. No primeiro protótipo, vamos nos restringir a primitivas geométricas.
Jogador

Nosso personagem é um quadrado. Durante a inicialização, definiremos seu tamanho e posição à esquerda, uma vez que estará no lugar. A posição do eixo Y dependerá do sorriso do jogador. Todos os valores absolutos serão calculados em relação ao tamanho da área de jogo. É mais fácil do que escolher tamanhos específicos - e teremos uma aparência aceitável em novos dispositivos.
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)
}
Bolo
Nosso personagem "corre" e tenta pegar bolos para marcar o máximo de pontos possível. Usamos a técnica padrão com a transição para o sistema de referência em relação ao jogador: ele ficará parado e os bolos voarão em sua direção. Se o quadrado do bolo cruzar com o quadrado do jogador, o ponto será contado. E se ao mesmo tempo pelo menos um olho do usuário estiver fechado - dois pontos ¯ \ _ (ツ) _ / ¯
Também em nosso universo haverá apenas um bolo de
//
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()
}
}
O que aconteceu
Vamos tornar a exibição de pontos muito simples. Exibiremos o número no centro da tela. Você só precisa levar em conta a altura do texto e recuar a parte superior para a beleza.
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
}
Vamos ver que tipo de brinquedo fizemos:
Diferença passo completo.
Grafônio
Para não ter vergonha de mostrar o jogo na apresentação do hackathon, vamos adicionar um pouco de grafonium!

Imagens
Partimos do fato de que não podemos desenhar gráficos impressionantes. Felizmente, existem sites com recursos de jogos gratuitos. Gostei deste , embora agora não esteja disponível diretamente por um motivo desconhecido para mim.

Animação
Nós desenhamos no Canvas, o que significa que precisamos implementar a animação nós mesmos. Se houver fotos com animação, será fácil programar. Apresentamos uma classe para um objeto com imagens variáveis.
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 obter o efeito do movimento, o fundo também deve ser animado. Ter uma série de quadros de fundo na memória é uma história aérea. Portanto, vamos fazer isso de forma mais astuta: desenharemos uma imagem com uma mudança de tempo. Esboço da ideia:

Etapa completa de comparação.
Resultado final
É difícil chamá-lo de obra-prima, mas é bom para um protótipo à noite. O código pode ser encontrado aqui . É executado localmente sem travessuras adicionais.
Concluindo, acrescentarei que a detecção de rosto do kit de ML pode ser útil para outros cenários.
Por exemplo, para tirar selfies perfeitas com amigos: você pode analisar todas as pessoas no quadro e garantir que todos sorriram e abriram os olhos. A detecção de vários rostos em um fluxo de vídeo funciona imediatamente, portanto, a tarefa não é difícil.
Usando o reconhecimento de contorno de rosto do módulo de Detecção de rosto, é possível replicar máscaras que agora são populares em quase todos os aplicativos de câmera. E se você adicionar interativo - através da definição de um sorriso e uma piscadela - então usá-los será duplamente divertido.
Essa funcionalidade - contorno facial - pode ser usada para mais do que entretenimento. Aqueles que tentaram cortar uma foto para documentos irão apreciar. Pegamos o contorno do rosto, recortamos automaticamente a foto com a proporção desejada e a posição correta da cabeça. O sensor giroscópio ajudará a determinar o ângulo correto de tiro.