Анимация и графика в играх: пошаговое обучение с Flame и Flutter (часть 2 из 5)

Перевод урока Game Graphics and Animation Tutorial – Step by Step with Flame and Flutter (Part 2 of 5)

В этой части обучения мы сфокусируемся на добавлении графики и анимации в нашу игру. Мы продолжим с того места, на котором остановились в предыдущей части: простая, но интерактивная казуальная игра.

Она претерпит некоторые изменения в своих характеристиках, и они будут касаться улучшения графики, добавления анимаций, создания движения.

Как и в предыдущих частях, в конце этой статьи будет небольшое демо-видео. Кстати, если вы их еще не читали — сейчас самое время, потому что работать сегодня мы будем с тем, что уже создали в первых двух частях.

Вот полный список серии:

  1. Введение в разработку игр
  2. Создание игры
  3. Графика и анимация (этот перевод статьи)
  4. Views (экраны) и диалоговые окна
  5. Счет, хранилище и звук
  6. Завершение: сборка и публикация игры

Подготовка

  1. Все необходимые программы вы можете найти в первой части этой серии
  2. Графические ресурсы (assets) — их можно взять на любом сайте с ресурсами для создания игр, к примеру, Open Game Art, только не забудьте упомянуть автора!
    Мы будем придерживаться прежних правил касательно кода (никаких ненужных ключевых слов и аннотаций, как @override и new) и обозначения пути файла (./ ссылается на директорию проекта).

От автора перевода: в 2020 году ключевое слово new не используют, а вот аннотации соблюдают. Что касается аннотации @override Dart анализатор кода предложет вам их добавить.

Что изменится, так это скриншоты для каждого минимального изменения в коде. Их станет меньше и они будут в более высоком разрешении (меньший текст), чтобы вместить большие файлы.

Весь код для этого обучения доступен для просмотра и загрузки на GitHub.

Графические ресурсы (assets)

На этом изображении (оно из предыдущей части) уже отображены графические ресурсы. Это изображения мух, взятые из Open Game Art. Они имеют лицензию CC0 , что эквивалентно общественному достоянию. Ресурсы находящиеся в общественном достоянии, могут быть использованы в любых целях.

Однако мы не будем использовать эти изображения, а возьмем другие из такого пакета ресурсов:

Нажмите на эту ссылку, чтобы его скачать!

Важное примечание: этот пакет может быть использован только при выполнении этого туториала. Он является частью проекта Langaw на GitHub, который имеет лицензию CC-BY-NC-ND.

Это означает, что вы можете делиться, копировать и распространять материалы в любой среде в любом формате, лишь с некоторыми ограничениями.

Больше о лицензии CC-BY-NC-ND можно узнать здесь.

Продолжим создание игры

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

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

Шаг 1: добавляем графические ресурсы (assets)

Сначала, скачайте пакет ресурсов со всеми необходимыми ресурсами, если вы еще этого не сделали.

Или же вы можете выбрать любой другой пакет из OpenGameArt.org, или даже создать свой собственный.

Примечание: открыв пакет с ресурсами, вы увидите пять разных видов мух, и каждый из них имеет три привязанных файла. Это потому, что у нас будет пять разных видов мух с разными возможностями, но об этом позже.

Добавление ресурсов в игру

Создайте директорию в корне проекта и назовите ее assets (ресурсы). В этой директории (./assets) создайте директорию с именем images.

Единственное требование, которое Flame выдвигает нашей структуре файла — ресурсы изображений должны находиться в ./assets/images.

Теперь мы можем упорядочить наши файлы/ресурсы как угодно — в том числе и просто оставить их как есть, потому что мы создаем весьма простую игру.

Но мы не будем так делать, так как в будущем вы будете создавать более сложные игры, и лучше не упускать возможность лишний раз попрактиковаться в организации и упорядочивании.

Скопируйте файлы fly в папку ./assets/images/flies, а файл фона — в ./assets/images/bg.

Проделав это, в ./assets/images вы получите следующее:

Регистрация ресурсов во Flutter

Прежде, чем мы сможем воспользоваться этими ресурсами, мы должны сообщить Flutter, что эти файлы должны быть включены в сборку приложения (при запуске отладки приложения или финальной сборке APK для распространения).

Чтобы это сделать, нам нужно указать имена файлов изображений в файле зависимостей ./pubspec.yaml. Найдите секцию flutter и добавить под нее подсекцию assets.

Под этой подсекцией нужно перечислить все виды мух, что мы только добавил в папку ресурсов:

Примечание: обратите внимание на отступы в pubspec.yaml. Этот файл использует два пробела для каждого отступа. Также убедитесь в правильности написания названий файлов. Не лишним будет запустить приложение, чтобы компилятор проверил, имеются у вас все названные выше файлы. Если нет, или если он найдет опечатку, появится окно об ошибке.

Так как мы разрабатываем незатейливую игру, мы можем пропустить экран загрузки, например, при переходе игрока в другую комнату, смене одежды, подключении к игре и так далее. Потому что в нашей игре всего этого попросту нет: только фон и парочка мух.

Зная это, мы можем просто загрузить все ресурсы при старте игры. Конечно, это займет пару миллисекунд и отобразится черный экран, но кто это заметит?

Откроем ./lib/main.dart и импортируем пакет Flame путем добавления следующей строки в самый верх файла:

Затем, внутри функции main добавьте следующий блок кода (сразу после настройки игры в полноэкранный режим и настройки ориентации):

Пояснение: на самом деле, это все — одна строка, расписанная по вертикали для удобочитаемости. Список String передается параметром в images метода loadAll. Он предзагружает файлы изображений, на которые указывает список String.

Эти изображения кэшированы в статичной переменной во Flame для того, чтобы их можно было неоднократно использовать позже.

Посмотреть код для этого шага на GitHub.

Шаг 2: меняем фоновый рисунок

Сейчас наш фон — просто сплошной цвет. Может, сам цвет и красивый, но обычно в играх фон является чем-то большим.

Это backyard.png из нашего пакета ресурсов.

Как вы видите, это изображение длинное по вертикали. Это потому, что нам важна лишь ширина (как обсуждалось в предыдущей части). Телефон может быть настолько длинным, насколько это только возможно (в пределах разумного), и наш фон все равно покроет весь экран полностью.

Компонент фоновое изображение

Не лишним будет отделить логику фонового рисунка в свой собственный компонент.
Создадим файл компонента ./lib/components/backyard.dart.

Файл описывает класс Backyard, у которого имеется конструктор и два других метода (таких, как игровой цикл и компонент мухи). Пока что мы не будем использовать метод update, но он понадобится нам позже, поэтому просто поместим его сюда.

Этот класс содержит одну final переменную экземпляра LangawGame, которая выступит в роли ссылки на экземпляр игры (и его свойства), содержащий этот компонент.

Другая переменная экземпляра — спрайт под названием bgSprite. Эта переменная содержит данные спрайта, которые будут отображены на экране.

Внутри конструктора мы инициализируем переменную этого спрайта путем создания нового Sprite и передачи названий файлов ресурсов, что нам нужны. Файл уже был загружен в ./lib/main.dart и готов к использованию без какого-либо ожидания загрузки.

Примечание: как и в остальных файлах этого проекта, код импорта находятся в самом верху. Импорт dart:ui дает нам доступ к классу Canvas. При импорте sprite.dart из пакета flame мы получаем доступ к классу Sprite. А импорт langaw-game.dart дает доступ к классу LangawGame.

Адаптация размеров

Если вы откроете фоновое изображение в фотошопе (или любом другом графическом редакторе), вы увидите, что размер рисунка равен 1080х2760 пикселей.

На самом деле, нам не важны ни эти, ни логические пиксели, так как мы используем Flutter. Все, что нам важно — это то, что фоновый рисунок в ширину занимает девять плиток.

Как показано в расчетах выше, изображение равно девяти плиткам в ширину и двадцати трем в высоту.

Рисуем фон

Добавим переменную экземпляра Rect под названием bgRect.

Внутри конструктора добавим этот блок кода, прямо под инициализацией свойства bgSprite:

Пояснение: это снова всего лишь одна строка, разбитая на несколько по вертикали для удобства. Четыре средние строки сообщают значения для левого края (х), верха (у), ширины и высоты соответственно.

Мы рисуем фон во всю ширину, поэтому начинаем с нуля для левой стороны и расширяемся в game.tileSize * 9. Мы также могли бы использовать game.screenSize.width, раз game.tileSize равен game.screenSize.width, поделенному на 9.

Мы знаем, что размер изображения в плитках — 9х23. Поэтому, чтобы нарисовать все изображение, мы просто задаем game.tileSize * 23 как высоту.

Наконец, top (y) — это отрицательное число, которое сообщает разницу размером экрана и фоновым изображением.

Если соотношение сторон мобильного устройства игрока 9:16, то высота экрана будет равна 16 * tile size. Если из этого результата мы вычтем 23 * tile size, то получим -7 * tile size. Это значит, что верхний край изображения находится выше края экрана на 7 плиток.

Согласно этим подсчетам, изображение всегда будет прикреплено к низу экрана.

Наконец, мы рисуем фоновое изображение, когда метод рендеринга этого компонента вызван:

Файл должен выглядеть так:

Добавляем фон к игре

Теперь, когда компонент фона готов, добавим его в логику игры. Откройте ./lib/langaw-game.dart.

Чтобы использовать класс Backyard, нам нужно импортировать файл класса, используя следующую строку:

Теперь нам нужно добавить новую переменную экземпляра под названием background с типом Backyard.

Внутри метода initialize инициализируйте новый объект Backyard и свяжите его с переменной экземпляра background. Это нужно сделать после того, как размер экрана определен, так как конструктор использует значения размера экрана и размера плитки.

Как и при создании мух, мы передаем данный экземпляр LangawGame при помощи ключевого слова this.

Теперь файл должен выглядеть так:

Затем, внутри метода рендеринга, мы вызываем метод рендеринга background и передаем ему Canvas.

Пока что у нас есть четыре линии, которые образуют прямоугольник со сплошной заливкой. Нам нужно их убрать и заменить следующей строкой:

Файл должен выглядеть так:

При запуске игры вы должны увидеть, что фоновый рисунок теперь отображается в игре.

Выглядит неплохо!
Посмотреть код для этого шага на GitHub.

Шаг 3: меняем мух

Всего у нас имеется пять разных видов мух. Мы не только сосредоточимся на их внешних различиях, но и подготовим их к изменению других свойств.

Это можно сделать при помощи наследования: это создание класса (подкласса) и расширение уже существующего класса (суперкласса).

Размер спрайта мухи

Уточнение: муха из ресурса пакета имеет размер на пол плитки больше во всех направлениях, чем прямоугольник касания (flyRect).

На изображении выше спрайты нарисованы внутри голубого квадрата (назовем это sprite box), но касание игрока должно произойти внутри красного квадрата (hit box, который в нашем коде называется flyRect).

Подготовка суперкласса

Прежде, чем создать подкласс, давайте убедимся, что наш суперкласс готов к расширению.

Мы будем использовать уже существующий класс Fly в качестве суперкласса, так что откроем ./lib/components/fly.dart. Класс Fly в этом файле будет иметь все те же возможности, что и все мухи.

Сначала удалим drawRect, так как мы не будем рисовать простой прямоугольник. Очистите метод рендеринга, чтобы он выглядел так:

Затем удалим все ссылки на flyPaint. Этот объект используется только для рисования простых прямоугольников.

Удалите из переменных экземпляров:

Затем удалите эти строки из конструктора:

Затем удалите из обработчика onTapDown следующую строку:

Мы все еще будем использовать flyRect как hit box, поэтому оставим его.

Добавляем спрайты

Для каждого экземпляра класса Fly (или какого-либо его подкласса), нам нужно подготовить и сохранить два набора спрайтов.

Один набор будет состоять из двух спрайтов, которые отобразятся друг за другом для создания анимации «полета». Для этого нам понадобится List.

В другом наборе будет всего один спрайт, которые отобразится при смерти мухи.

Также нам понадобится еще одна переменная экземпляра, в которой будет храниться информация, какой из спрайтов будет показываться для анимации полета.

Импортируем sprite.dart из Flame.

И добавим следующий блок кода в раздел переменных экземпляра:

Примечание: в этом классе переменные спрайтов не будут инициализированы, так как каждый подкласс будет использовать разные спрайты.

Внутри метода render, отрендерим спрайт в зависимости от состояния мужи (жива или мертва):

Пояснение: метод render определяет, какой спрайт показывать, путем проверки переменной isDead. Если текущее состояние — «мертвая», deadSprite рендерится. Если нет, рендерится первый предмет в списке flyingSprite.

Что касается flyingSpriteIndex.toInt(): доступ к элементам списка и массива осуществляется с помощью индекса int (целым числом). Однако, flyingSpriteIndex тип double, поэтому нам надо сначала конвертировать в int. Переменная double, потому что мы увеличим ее при помощи значений дельты времени (которая является double) в методе update.

Последняя часть, .inflate(2) , просто создает копию прямоугольника, который был вызван, но расширенную (в нашем случае, в два раза) от центра. Мы используем двойку как значение, потому что если вы посмотри на изображение выше, вы заметите, что голубой квадрат (sprite box) в два раза больше красного квадрата (hit box).

Класс Fly должен выглядеть так:

Создание первого подкласса

Давайте создадим наш первый вид мухи. Это будет самый простой вариант, наиболее «нормальный». Назовем его HouseFly (домашняя муха).

В директории ./lib/components создадим новый файл и назовем его house-fly.dart.

Откроем его и создадим обычный класс компонентов, но на этот раз, наследуя класс Fly.

Пояснение: Мы импортируем пакеты, необходимые для доступа к классам, от которых зависит наш новый класс. Затем мы объявляем класс под названием HouseFly и наследую его от класса Fly, тем самым создавая подкласс.

Подклассы имеют доступ к переменным и методы своего суперкласса и могут их переопределять.

Наш конструктор вызывает конструктор суперкласса super, что говорит программе запускать конструктор суперкласса перед выполнением кода внутри тела конструктора. Конструктор просто отображает параметры, необходимые конструктору суперкласса, и передает их во время вызова в super.

Внутри конструктора мы инициализируем переменную flyingSprite, которую этот подкласс унаследовал из класса Fly, путем создания нового экземпляра списка спрайтов. Затем добавляем два спрайта, которые соответствуют двум кадрам анимации полета, к этому списку.

Затем загружаем «мертвое» изображение мухи в Sprite и привязываем его к deadSprite.

Мы не будем переписывать методы рендеринга и обновления, так как эта муха не будет делать ничего особенного.

Добавление новой мухи

Теперь нам нужно отредактировать метод spawnFly, чтобы он создавал HouseFly вместо суперкласса Fly. Откроем ./lib/langaw-game.dart.

В разделе импорта (верх файла) импортируем подкласс, который мы только что создали, с помощью следующей строки:

Чтобы вместо Fly появлялась HouseFly, нужно заменить эту строку:

На эту:

Теперь файл должен выглядеть так:

Попробуем запустить игру!

Посмотреть код для этого шага на GitHub.

Шаг 4: другие виды мух

Этот шаг не займет много времени. Мы просто добавим подклассы для каждого вида мух.

Примечание: для последующих частей, описывающих варианты мух, я не буду описывать или разъяснять код, так как он точно такой же, как у класса домашних мух. Единственная разница — ссылка на соответствующее название файла изображения.

Слюнявая муха

Создаем новый файл класса ./lib/components/drooler-fly.dart:

Проворная муха

Создаем новый файл класса ./lib/components/agile-fly.dart:

Муха-мачо

Создаем новый файл класса ./lib/components/macho-fly.dart:

Прожорливая муха

Создаем новый файл класса ./lib/components/hungry-fly.dart:

Делаем вид мухи случайным

Теперь, когда у нас есть пять разных видов мух, давайте сделаем так, чтобы каждый раз, когда новая муха появлялась, она рандомно была одного из этих видов.

Импортируем все файлы классов видов мух в ./lib/langaw-game.dart:

Внутри метода spawnFly заменим эту строку:

На этот блок:

Пояснение: сначала мы получаем случайное целое число из rnd, используя метод nextInt. Цифра 5 означает, что нам нужно случайное целое число из пяти значений. Большинство языков программирования начинают отсчет с нуля, поэтому этими пятью значениями будут [0, 1, 2, 3, 4].

Полученное случайное значение передается в блок switch. Этот блок выполняет код, основываясь на переданные ему значения. К примеру, если ему передано значение 2, он выполняет flies.add(AgileFly(this, x, y));, и появляется AgileFly (проворная муха).

Утверждения break делают так, чтобы код под ними не был запущен. Чтобы узнать больше про эти блоки, прочитайте этот тур по языку Dart.

Метод spawnFly теперь выглядит так:

Посмотреть код для этого шага на GitHub.

Запустите игру, чтобы убедиться, что, каждый раз, когда появляется муха, она имеет другой вид, чем предыдущая. Однако, так как это выбирается случайно, есть возможность того, что муха одного вида появится два раза подряд.

Шаг 5: заставляем мух летать

Пока что все идет отлично: у нас есть интерактивная игра с хорошей графикой и даже разными вариантами видов игровых объектов.

Но мы еще не закончили.

Анимация мух

Мухи не парят совершенно неподвижно в воздухе, им нужно махать крылышками, чтобы не упасть и продолжить свой полет.

Пакет ресурсов, который мы используем, предоставляет нам все необходимые кадры для анимации мух. Мы уже даже загрузили их и подготовили спрайты в каждой переменной.

Откроем ./lib/components/fly.dart.В методе update поместите блок else (else { }) в конец блока if (isDead).

Внутри блока else впиши следующие строки:

Пояснение: сначала мы добавили к переменной flyingSpriteIndex значение 30, умноженное на дельту времени. Помните, что эта переменная становится int во время рендеринга и ее значение int используется для определения того, какой кадр показывать: либо первый (индекс 0), либо второй (индекс 1).

Мы стараемся достичь 15 взмахов (15циклов анимации) в секунду. Так как у нас есть два кадра для каждого цикла, мы будем отображать 30 кадров в секунду.

Скажем, что игра работает при 60 кадрах в секунду. Метод обновления будет запускаться примерно каждые 16,6 миллисекунды (что является значением переменной дельты времени, но в секундах). Начальное значение для flyingSpriteIndex — ноль.

Первый кадр: 30 * 0.0166 прибавляется к flyingSpriteIndex. Теперь значение flyingSpriteIndex равняется 0.498. При запуске .toInt() с этим значение, вы получите 0 и первое изображение.

Второй кадр: проделываем все то же самое и получаем значение 0.996. При запуске .toInt() с этим значением, вы все еще получите ноль и первое изображение.

Третий кадр: снова совершаем те же подсчеты и получаем 1.494. При запуске .toInt() получаем 1 и второе изображение.

Четвертый кадр: получаем 1.992, а затем 1 и, следовательно, второе изображение.

Но в пятом кадре мы получим значение 2.49.

Блок if перезапускает переменную flyingSpriteIndex, если ее значение становится больше или равно двум, так как у нас нет третьего изображения (с индексом 2).

Мы вычитаем 2 из 2.49 и получаем 0.49, что имеет значение .toInt() равным 0 и показывает первое изображение снова.

И это происходит вновь и вновь, циклом между двумя изображениями в 15 циклов в секунду.

Примечание: согласно подсчетам, периодически изображение будет показываться на протяжении трех фреймов. Но на самом деле, это не наш случай, потому что мы не использовали точные значения: 1 секунда, разделенная на 60 кадров в секунду, равняется не 0.0166, а 0.016666… с бесконечным количеством 6 на конце. Если умножить это значение на 30, мы получим 0.5. Также, дельта времени не всегда равна 0.01666666. Так что выполнение расчета действительно соответствует логике 15 махов в секунду. Но, даже если у нас и появится изображение на три кадра, при 60 фреймах в секунду это будет практически незаметно.

Теперь метод обновления выглядит так:

Время запустить игру и проверить, как наши мухи машут крылышками. Конечно, скриншот не сможет это показать, так что просто попробуйте сами.

Соответствие размеров мух

Теперь, когда у нас есть мухи, и даже анимированные, вы можете заметить, что размер всех мух одинаковый и соответствует размеру тайла.

Нужно закрепить для каждого вида мухи соответствующий ей размер.

Чтобы это сделать, нам нужно отредактировать ./lib/components/fly.dart , убрать инициализацию для flyRect и передать ее каждому подклассу, так как каждый вид мухи будет иметь свой размер.

Удалите следующую строку из конструктора:

В Dart, если у вас есть пустой конструктор, который дает значения финальным переменным экземпляра, вы можете опустить часть тела и закончить ее точкой с запятой:

По факту, мы больше не будем использовать параметры х и у, так как прямоугольник больше не проходит инициализацию, так что просто удалим их. Теперь конструктор выглядит так:

Затем откроем ./lib/components/house-fly.dart и отредактируем вызов super в конструкторе, чтобы он не передавал значения х и у. Мы просто удалили эти значения в конструкторе Fly.

Открывающая строка в конструкторе **HouseFly **должна выглядеть так:

Затем, внутри конструктор, мы добавляем инициализацию flyRect, которую только что убрали из Fly.

Так как мы теперь используем в этом файле Rect, нам нужно импортировать пакет Dart ui:

Проделайте те же изменения с другими вариантами.

Домашняя муха, слюнявая и проворная мухи будут одного размера, но нам нужно сделать их больше.

Для всех этих видов (./lib/components/house-fly.dart, ./lib/components/drooler-fly.dart, и ./lib/components/agile-fly.dart), поменяйте инициализацию flyRect внутри конструктора:

Теперь наш hit box не такого же размера, что game.tileSize, а больше в 1.5 раза. Это теперь наш основной размер.

Sprite box тоже увеличится, так как он привязан к hit box.

Мачо-муха будет в 1.35 раз больше остальных мух.

Измените инициализацию flyRect:

Проделаем то же самое для прожорливой мухи, но используем 1.5 x 1.1 = 1.65 как параметр размера.

Теперь наша самая большая муха больше 2.025 раз, чем game.tileSize. Перейдем в ./lib/langaw-game.dart и обновим максимальные значения для х и у в методе spawnFly:

Запустите игру, и вы увидите, что мухи стали большего размера и соответствуют относительно друг друга.

Вы можете поэкспериментировать и назначить свои собственные значения размеров!

Перемещение мух

В реальной жизни, мухи не парят на одном месте, ничего не делая; они летаю вокруг, снуют взад-вперед и передвигаются по совершенно непредсказуемым и хаотичным траекториям.

Мы попытаемся сымитировать такое поведение, заставить их летать в случайные места экрана.

Сначала добавим свойство под названием speed . Это скорость, с которой мухи будут двигаться, и не у всех она будет одинакова.

Свойство — это просто другое название для переменной экземпляра. В нашем случае, разница заключается в определении и использовании их. Мы будем задавать свойство путем определения геттера.

Откроем ./lib/components/fly.dart и добавим туда свойство speed.

Мы будем использовать значение по умолчанию game.tileSize * 3, чтобы мухи могли пересекать экран не больше, чем за две секунды.

Вы, естественно, можете поэкспериментировать с разными значениями.

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

Им нужна цель, точка, которую необходимо достичь перед сменой направления.

Добавим еще одно переменную экземпляра под названием targetLocation с типом Offset. Мы используем класс offset, потому что он обладает такими полезными свойствами, как расчет направления, дистанции, размеров.

Продолжим с методом многократного использования для смены targetLocation.

Пояснение: как и в случае со spawnFly в ./lib/langaw-game, мы инициализируем две переменные (х и у) со случайными значениями, используя те же правила для максимума: мухи могут лететь только в ту точку, где они могут появляться.

Затем, в конструкторе, вызовем этот метод, чтобы у нас была цель-локация, не равная нулю, при появлении экземпляра мухи:

Файл fly.dart должен выглядеть так:

Теперь займемся самим движением. Внутри метода обновления и актуальным состоянием, что данный экземпляр мухи не «мертв» (isDead не равняется true), мы перемещаем муху к ее цели, учитывая значение дельты времени. Если она достигла своей цели, мы вызываем setTargetLocation, чтобы сделать новую цель случайной.

Введите блок кода внутри метода обновления под шагом flyingSpriteIndex.

Пояснение: сначала мы определяем переменную stepDistance расстояния шага, которая будет содержать, на сколько мы должны переместить муху. Если скорость - это то, сколько муха может продвинуться за одну секунду, то мы умножаем ее на дельту времени (t, количество времени, прошедшее с момента запуска метода обновления), что дает нам количество расстояния, которое муха должна была пройти за пройденное время.

Затем мы задаем новое значение Offset*, которое представляет собой "смещение" от текущего местоположения мухи до ее **targetLocation. Здесь мы используем операцию вычитания смещения, которая встроена в класс Offset.

Если муха в данный момент находится на отметке 50, 50, а целевое местоположение равно 120, 70, то это для toTarget ** будет иметь значение **(120 - 50), (70 - 50) или 70, 20.

Затем мы проверяем, меньше ли stepDistance расстояния ** toTarget.distance** (полезное свойство класса Offset, поэтому нам не нужно все вычислять вручную). Если это так, то мы все еще далеко от места назначения, поэтому мы продолжаем перемещать муху.

Чтобы переместить муху, мы создаем новое смещение, используя factory конструктор fromDirection . Этот конструктор принимает направление и дополнительное расстояние (которое по умолчанию равно 1). Для направления мы просто подаем свойство направления toTarget (еще одно полезное свойство класса Offset, поэтому нам не нужно возиться с тригонометрией для вычисления углов). Для расстояния мы вводим наше уже рассчитанное значение stepDistance расстояния шага.

Если stepDistance больше или равно свойству totarget distance, это означает, что муха находится очень близко к целевому местоположению и к этому моменту можно с уверенностью сказать, что она достигла своей цели. Поэтому мы просто перемещаем муху к цели, используя значение в toTarget, которое является фактическим расстоянием от мухи до targetLocation. Наконец, мы вызываем setTargetLocation(), чтобы дать нашей мухе новую цель.

Теперь у нас должен быть метод update, который выглядит следующим образом (я добавил комментарии, чтобы его было легче читать):

Проверьте это, запустив игру, чтобы вы могли видеть, как они двигаются.

Разные мухи; разные способности

Мы почти закончили с этой частью. Давайте сделаем несколько правок, чтобы дать некоторым из наших мух уникальные способности.

Для проворной мухи (./lib/components/agile-fly.dart), переопределите свойство speed и дайте ему коэффициент скорости пять. Почему пять? Потому что они проворны!

Код выглядит так:

Слюнявая муха (./lib/components/drooler-fly.dart) - ленивая муха. Он движется ровно вполовину быстрее, чем обычная домашняя муха.

Мачо-муха (./lib / components/macho-fly.dart) имеет огромные мускулы и большой размер. Давайте дадим ей значение 2.5, просто немного медленнее, чем средняя домашняя муха.

Посмотреть код для этого шага на GitHub.

Тест и демонстрация!

Если вы запускаете игру, у вас должно быть что-то вроде этого:

Примечание: На самом деле у нас есть пара ошибок, которые мы рассмотрим в последующих частях. Одна из них заключается в том, что если вы нажмете на муху, она умрет и породит другую. Если вы нажмете на нее еще раз во время падения, она породит еще одну муху!

Заключение

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

Используя приятную графику, анимацию, движение и вариации; простая игра «коробка, которая падает при нажатии» теперь стала играбельной.

Я надеюсь, что вы получили удовольствие, изучая анимацию и методы движения, описанные в этой части.

Подборка заметок