[Разработка игр, Разработка под Android, Хакатоны] Делаем игру с управлением улыбкой

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
11-Авг-2020 15:33

Привет! Меня зовут Иван Шафран, недавно я присоединился к команде видео ВКонтакте в роли программиста-разработчика для Android. Участвую в создании как продуктовых приложений, так и SDK. Время от времени я посещаю хакатоны, где можно реализовывать любые безумные идеи. Сегодня расскажу, как за пару часов сделать прототип мобильной игры с необычным управлением: персонаж будет реагировать на улыбку и подмигивание.

оригинал
Как возникла идея
Мысль создать такую игру пришла как раз во время хакатона. Формат предполагал, что на разработку есть один рабочий день, то есть 8 часов. Чтобы успеть сделать прототип, я выбрал Android SDK. Возможно, лучше подошли бы игровые движки, но в них я не разбираюсь.
Концепцию управления с помощью эмоций подсказала другая игра: там движения персонажа можно было задавать, меняя громкость своего голоса. Может, и эмоции уже кто-то использовал в игровом управлении. Но я знаю мало таких примеров, поэтому остановился на этом формате.
Осторожно громкое видео!

Извините, данный ресурс не поддреживается. :(
Настраиваем окружение для разработки
Нам понадобится только Android Studio на компьютере. Если нет реального устройства на Android для запуска, можно воспользоваться эмулятором с включённой веб-камерой.
Создаём проект с ML Kit

ML Kit — отличный инструмент, который поможет впечатлить жюри хакатона: ведь вы используете AI в прототипе! А вообще он помогает встраивать в проекты решения на основе машинного обучения, например функциональность для определения объектов в кадре, перевода и распознавания текста.
Для нас важно, что у ML Kit есть бесплатный offline API для распознавания улыбки и открытых или закрытых глаз.
Раньше, чтобы создать любой проект с ML Kit, нужно было сначала зарегистрироваться в консоли Firebase. Теперь этот шаг можно пропустить для офлайн-функциональности.
Android-приложение
Удаляем лишнее
Чтобы не писать логику по работе с камерой с нуля, возьмём официальный семпл и уберём из него то, что нам не нужно.

Для начала скачайте пример и попробуйте запустить. Исследуйте режим Face detection: выглядеть это будет, как на превью статьи.
Манифест
Начнём правки с AndroidManifest.xml. Удалим все теги activity, кроме первого. А на его место выставим CameraXLivePreviewActivity, чтобы сразу запускаться с камеры. В значении атрибута android:value оставляем только face, чтобы исключить из 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>

Полный diff шага.
Камера
Сэкономим время — не будем удалять лишние файлы, вместо этого сконцентрируемся на элементах экрана CameraXLivePreviewActivity.
  • На строке 117 установим режим face detection:
    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);
    }

Полный diff шага.
Распознаём эмоции
Распознавание улыбки по умолчанию выключено, но запустить его очень просто. Не зря же мы брали пример кода за основу! Выделим необходимые нам параметры в отдельный класс и объявим интерфейс слушателя:

FaceDetectorProcessor.java

SPL
// В классе 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, в котором покажем эмоции через смайлы.

Полный diff шага.
Разделяй и играй
Раз мы делаем игру, нужно место для рисования элементов. Будем считать, что она запускается на телефоне в портретном режиме. Значит, разделим экран на две части: камера сверху и игра снизу.
Контролировать персонажа с помощью улыбки сложно, к тому же на хакатоне мало времени для реализации продвинутой механики. Поэтому наш персонаж будет собирать ништяки по дороге, находясь либо в верхней части игрового поля, либо в нижней. Действия с закрытыми или открытыми глазами добавим как усложнение игры: поймали ништяк с закрытым глазом — очки удваиваются (либо пол-экрана не видно и можно грабить корованы).
Если хотите реализовать другой игровой процесс, то могу подсказать несколько занятных вариантов:
  • Guitar Hero / Just Dance — аналог, где под музыку нужно показывать определённую эмоцию;
  • гонка с преодолением препятствий, где нужно доехать до финиша за определённое время или не разбившись;
  • шутер, где подмигиванием игрок делает выстрел в противника.

Отображать игру будем в кастомном Android View — там в методе 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)
}

Тортик
Наш персонаж «бежит» и пытается ловить тортики, чтобы набрать как можно больше очков. Мы используем стандартный приём с переходом в систему отсчёта относительно игрока: он будет стоять на месте, а тортики — лететь к нему. Если квадрат тортика пересекается с квадратом игрока, то засчитываем балл. А если при этом хотя бы один глаз у пользователя закрыт — два балла ¯ \ _ ( ツ ) _ / ¯
Также в нашей вселенной будет всего один электрон тортик. Как только персонаж его съедает, он перемещается за экран на случайную полосу со случайной координатой. Так улыбка игрока не войдёт в резонанс при предсказуемом появлении тортика.
// При инициализации тортика сразу перемещаем его за экран
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
}

Смотрим, какую игрушку мы сделали:
Извините, данный ресурс не поддреживается. :(
Полный diff шага.
Графоний
Чтобы игру было не стыдно показывать на презентации хакатона, добавим немного графония!

Картинки
Исходим из того, что рисовать впечатляющую графику мы не умеем. К счастью, есть сайты с бесплатными ассетами для игр. Мне понравился вот этот, хотя сейчас он недоступен напрямую по неизвестной мне причине.

Анимация
Мы рисуем на 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()]
    }
}

Чтобы получился эффект движения, фон тоже должен быть анимированным. Иметь серию кадров фона в памяти — накладная история. Поэтому поступим хитрее: одно изображение будем рисовать со сдвигом по времени. Схема идеи:

Полный diff шага.
Финальный результат
Сложно назвать это шедевром, но для прототипа за вечер сойдёт. Код можно найти тут. Запускается локально без дополнительных махинаций.
Извините, данный ресурс не поддреживается. :(
В заключение добавлю, что ML Kit Face Detection может пригодиться и для других сценариев.
Например, чтобы делать идеальные селфи с друзьями: можно анализировать всех людей в кадре и убеждаться, что все улыбнулись и открыли глаза. Определение нескольких лиц в видеопотоке работает из коробки, поэтому задача несложная.
Используя распознавание контуров лица из модуля Face Detection, реально повторить маски, которые сейчас популярны почти во всех приложениях с камерой. А если добавить интерактив — через определение улыбки и подмигивания, — то пользоваться ими будет вдвойне весело.
Эту функциональность — определение контуров лица — можно применять не только для развлечений. Те, кто сами пытались вырезать фото на документы, оценят. Берём контур лица, автоматически вырезаем фото с нужным соотношением сторон и правильным положением головы. Определить правильный угол съёмки поможет датчик гироскопа.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_razrabotka_igr (Разработка игр), #_razrabotka_pod_android (Разработка под Android), #_hakatony (Хакатоны), #_android, #_hakaton (хакатон), #_ml_kit, #_blog_kompanii_vkontakte (
Блог компании ВКонтакте
)
, #_razrabotka_igr (
Разработка игр
)
, #_razrabotka_pod_android (
Разработка под Android
)
, #_hakatony (
Хакатоны
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 16:34
Часовой пояс: UTC + 5