Пишем игру для Android. Часть 4 — Спрайтовая анимация
На прошлых уроках мы говорили о работе с графикой. Мы вывели на экран изображение робота и заставили его двигаться. Однако пока наша поделка выглядит довольно тускло. Чтобы вдохнуть в наш объект жизнь, необходимо добавить анимацию, заставить поднимать ноги и дрыгать ручками в процессе перемещения по экрану.
Давайте на какое-то время отвлечемся от игры и поговорим об анимации вообще. Представим, что нам требуется нарисовать человечка, который шагает слева направо по экрану. Как это можно реализовать? Обычно новичкам эта задача кажется непомерно трудной. На самом деле здесь нет ничего сложного. Идея взята из кинематографа. У нас должен быть набор изображений, представляющий собой «фотографии» нашего человечка в разные, достаточно близкие, моменты времени. Быстро меняя кадры, мы увидим, что наша картинка начала двигаться. Ситуацию с походкой упрощает тот факт, что она носит периодический характер. Грубо говоря, чтобы получить красивую и достоверную анимацию нам достаточно иметь кадры с момента, когда человечек опирается, скажем, на левую ногу, до момента, когда он сделав два шага вновь на нее обопрется.
Давайте рассмотрим пример, состоящий из нескольких кадров. Не будем заморачиваться на графике и возьмем готовый рисунок из некогда популярной компьютерной игры Monkey Island.
В компьютерной графике и gamedev-е широко используется понятиеспрайт(sprite). В современной трактовке оно означает графический объект, который может перемешаться по экрану и при этом изменять свой вид. Со спрайтом связан рисунок, содержащий раскадровку и, в зависимости от ситуации, спрайт имеет вид того или иного кадра. Ниже приведет пример рисунка с последовательностью кадров, который будет использоваться нами для создания спрайта.
Это изображение имеет ширину в 150 точек, и содержит 5 кадров, то есть ширина каждого кадра составляет 30 пикселей.
Чтобы создать анимацию мы можем загрузить каждый кадр, как отдельную картинку и затем через равные интервалы времени выводить их последовательно на экран. Можно сделать по-другому: загрузить одну картинку, содержащую все кадры, а затем налету разбивать ее на кадры и выводить требуемый в данный момент кадр. На самом деле это довольно просто. Мы знаем, что наша картинка содержит 5 кадров шириной 30 пикселей. Определим прямоугольник, имеющий ширину кадра (30 точек) и высоту всего изображения. На приведенном ниже рисунке синими прямоугольниками отмечены первые два кадра.
Давайте продолжим разработку нашей игры. Создадим проект. За основу возьмем разработанный на предыдущих уроках пример Droid_03. Добавим в папку res/drawable-mdpi проекта файлwalk_elaine.png Создадим новый класс для нашего персонажа. Поскольку в Monkey Island персонаж зовут Elaine, назовем класс ElaineAnimated.
Здесь bitmap — png рисунок, содержащий все кадры; sourceRect — прямоугольная область, которая «очерчивает» в рисунке границы текущего кадра; frameTicker — переменная, содержащая время, которое прошло с момента последной смены кадра анимации. Указанная в комментарии переменная fps обозначает не fps игры, а fps спрайта, то есть сколько раз изменяется кадр спрайта за секунду. Для плавной анимации это значение должно иметь величину порядка 25-30, однако для наших учебных целей подойдет и более скромное число 5. Мы не можем поставить здесь большее значение, поскольку у нас всего 5 кадров в рисунке. framePeriod — период между сменой кадра в миллисекундах. Для нашего случая, когда мы хотим менять 5 кадров в секунду эта величина равна 200 мс.
Подразумевается, что кадры имеют одинаковую ширину, поэтому spriteWidth вычисляется, как общая ширина рисунков поделенная на число кадров. В конструктор передается параметр fps, который обозначает число кадров спрайта в секунду.
Добавим в конструктор класса MainGamePanel строку, создающую объект для нашего нового анимированного персонажа
Добавим в класс метод update, который будет изменять текущий кадр в зависимости от текущего времени. Мы специально не привязываемся к частоте обновления игрового цикла, чтобы не нарушать процессор лишней работой. Например, допустим, что наша программа запущена на очень продвинутом телефоне, который обеспечивает проход итерации игрового цикла за 20 мс, допустим также, что менять картинку спрайта нужно раз в 200 мс, таким образом, мы должны обновлять спрайт каждый десятый шаг игрового цикла.
Этот метод получает в качестве параметра текущее время и если это время превышает сумму времени последнего обновления (frameTicker) и длительности показа кадра (framePeriod), то необходимо перейти к показу следующего кадра. Для этого увеличиваем на единицу значение переменная currentFrame, а затем на основании ее значения вычисляем заново границы кадра (sourceRect.left и sourceRect.right).
Внесем изменение в метод update класса MainGamePanel
Теперь у нашего спрайта меняются кадры, но мы не видим этого. Добавим в класс ElaineAnimated метод draw, который будет выводить на экран текущий кадр
Команда canvas.drawBitmap рисует прямоугольную область sourceRect из рисунка bitmap в прямоугольной области destRect.
Изменим также метод onDraw, добавив туда вызов метода перерисовки спрайта
Вообще на этом можно было бы остановиться. Все работает, картинки меняются, человечек дрыгает ножками и машет ручками, но чтобы окончательно разобраться в том, как собственно происходит смена кадров, давайте чуть-чуть модифицируем метод draw. Нарисуем ниже анимированного спрайта всю картинку bitmap и будем поверх нее выводить прозрачный зеленый квадратик, соответствующий текущему слайду. Практической пользы в этом, конечно, нет, но для общего понимания идеологии работы со спрайтами весьма полезно
Источник
Пишем движок игры под Android – tutorial часть 3 (спрайты)
В прошлый раз мы с Вами занимались всякими простенькими геометрическими объектами. Конечно они тоже нужны, но красивую игру с ними не сделаешь. Так что думаю самое время перейти к более серьезным вещам и заняться спрайтами. Что такое спрайт? ну это некоторое растровое изображение, которое можно перемещать по экрану из кода нашей программы. На самом деле грамотно используя спрайтовую графику можно создавать очень даже приятные игры. Работать со спрайтами в Andriod на самом деле очень легко. Для нас в SDK есть целая куча классов, которые спешат прийти на помощь. Например класс Bitmap. Вот уж воистину замечательное подспорье! Посмотрим чем он может нам помочь? Начнем описывать класс mSimpleSprite с того, что для разнообразия унаследуем его от mPoint. Таким образом каждый спрайт у нас уже имеет некоторую позицию на экране. И не только позицию, но еще и скорость движения и ускорение!
Собственно вот весь код, и давайте теперь разбираться что здесь к чему и почему. Как же работает наш спрайт и какие у него есть возможности?
Начнем как обычно с полей класса. Ну самое главное — это сама картинка которая будет храниться в поле bmp, которое есть не что иное как экземпляр класса Bitmap. Также здесь у нас есть поля width и height — в которых хранится ширина и высота изображения в пикселах. На всякий случай я добавил поля rpx, rpy и centralAxis — которые когданить обеспечат вращение спрайта вокруг точки (правда пока что я этого функционала не реализовал). Зато вместе с изображением в спрайте хранится матрица преобразований matrix. А еще у нашего спрайта получлась целая куча конструкторов. Давайте разберемся зачем их так много.
Дело в том что изображение в Bitmap можно загрузить из разных типов ресурсов. Возможно Вы заметили, что в Вашем проекте есть папка res. Это папка в которой наше приложение хранит ресурсы. При чем для каждого типа ресурсов в этой папке есть отдельный каталог. Для всего содержимого этой папки в классе R.java (находится в папке gen) генерируются дескрипторы. С помощью такого дескриптора изображение легко загрузить в Bitmap статическим методом класса BitmapFactory.decodeResource(Res, ID). Также есть папка assets, в которой хранятся так называемые raw ресурсы, которые тоже легко можно загрузить, представив их в виде потока. Ну и конечно можно загружать изображения хранящиеся на диске.
Собственно все эти методы и представлены в конструкторах. Так что использовать класс mSimpleSprite должно быть легко и приятно :-).
На всякий случай написал еще метод resize(). Он позволяет изменять размер изображения, хранящегося в нашем спрайте.
Метод refreshAll() обновляет дополнительную информацию о картинке, такую как ее ширина и высота. Вызывается в коде конструкторов, а так же при изменении размеров. Можно вызывать и в других случаях. Ну а все остальные методы думаю вообще не вызовут никаких затруднений. Единственное, что в методе autoSize() используются некоторые статичные члены класса mSettings, который я пока что здесь не привел. Это класс с глобальными настройками нашего приложения. После его инициализации в нем хранятся наиболее основные настройки движка. То есть если в классе mSettings переменная AutoScale равна истине, то все загружаемые спрайты будут автоматически изменять размер под любое разрешение экрана.
А вот код класса mSettings:
Как видите, ничего сложного! Названия переменных говорят сами за себя. Работает это примерно так. Во время запуска программы мы инициализируем этот класс методом GenerateSettings(). Этот метод в качестве параметров может принимать объект типа Display или просто ширину и высоту экрана в пикселах. Это — ширина и высота экрана конкретного устройства. А в переменных DefaultXRes и DefaultYRes хранится ширина и высота экрана для которого вы подготовили графику. Исходя их этих значений высчитываются значения переменных ScaleFactorX и ScaleFactorY — то есть фактически множители масштабирования. Именно они и используются в методе AutoScale() класса mSimpleBitmap.
Правда есть еще какой-то там загадочный таймер, но что он такое и какую роль здесь играет, оставим на потом.
Код классов можно скачать здесь: androidTutor3. Если есть вопросы или комментарии милости просим
Тут их можно оставить 🙂 Или написать мне на mail
Источник
Туториал: Создание простейшей 2D игры на андроид
Этот туториал предназначен в первую очередь для новичков в разработке под андроид, но может быть будет полезен и более опытным разработчикам. Тут рассказано как создать простейшую 2D игру на анроиде без использования каких-либо игровых движков. Для этого я использовал Android Studio, но можно использовать любую другую соответствующее настроенную среду разработки.
Шаг 1. Придумываем идею игры
Для примера возьмём довольно простую идею:
Внизу экрана — космический корабль. Он может двигаться влево и вправо по нажатию соответствующих кнопок. Сверху вертикально вниз движутся астероиды. Они появляются по всей ширине экрана и двигаются с разной скоростью. Корабль должен уворачиваться от метеоритов как можно дольше. Если метеорит попадает в него — игра окончена.
Шаг 2. Создаём проект
В Android Studio в верхнем меню выбираем File → New → New Project.
Тут вводим название приложения, домен и путь. Нажимаем Next.
Тут можно ввести версию андроид. Также можно выбрать андроид часы и телевизор. Но я не уверен что наше приложение на всём этом будет работать. Так что лучше введите всё как на скриншоте. Нажимаем Next.
Тут обязательно выбираем Empty Activity. И жмём Next.
Тут оставляем всё как есть и жмём Finish. Итак проект создан. Переходим ко третьему шагу.
Шаг 3. Добавляем картинки
Скачиваем архив с картинками и распаковываем его.
Находим папку drawable и копируем туда картинки.
Позже они нам понадобятся.
Шаг 4. Создаём layout
Находим activity_main.xml, открываем вкладку Text и вставляем туда это:
На вкладке Design видно как наш layout будет выглядеть.
Сверху поле в котором будет сама игра, а снизу кнопки управления Left и Right. Про layout можно написать отдельную статью, и не одну. Я не буду на этом подробно останавливаться. Про это можно почитать тут.
Шаг 5. Редактируем MainActivity класс
В первую очередь в определение класса добавляем implements View.OnTouchListener. Определение класса теперь будет таким:
Добавим в класс нужные нам статические переменные (переменные класса):
В процедуру protected void onCreate(Bundle savedInstanceState) <
добавляем строки:
Классы LinearLayout, Button и т.д. подсвечены красным потому что ещё не добавлены в Import.
Чтобы добавить в Import и убрать красную подсветку нужно для каждого нажать Alt+Enter.
GameView будет подсвечено красным потому-что этого класса ещё нет. Мы создадим его позже.
Теперь добавляем процедуру:
Если кто-то запутался ― вот так в результате должен выглядеть MainActivity класс:
Итак, класс MainActivity готов! В нём инициирован ещё не созданный класс GameView. И когда нажата левая кнопка — статическая переменная isLeftPressed = true, а когда правая — isRightPressed = true. Это в общем то и всё что он делает.
Для начала сделаем чтобы на экране отображался космический корабль, и чтобы он двигался по нажатию управляющих кнопок. Астероиды оставим на потом.
Шаг 6. Создаём класс GameView
Теперь наконец-то создадим тот самый недостающий класс GameView. Итак приступим. В определение класса добавим extends SurfaceView implements Runnable. Мобильные устройства имею разные разрешения экрана. Это может быть старенький маленький телефон с разрешением 480×800, или большой планшет 1800×2560. Для того чтобы игра выглядела на всех устройствах одинаково я поделил экран на 20 частей по горизонтали и 28 по вертикали. Полученную единицу измерения я назвал юнит. Можно выбрать и другие числа. Главное чтобы отношение между ними примерно сохранялось, иначе изображение будет вытянутым или сжатым.
unitW и unitW мы вычислим позже. Также нам понадобятся и другие переменные:
Конструктор будет таким:
Метод run() будет содержать бесконечный цикл. В начале цикла выполняется метод update()
который будет вычислять новые координаты корабля. Потом метод draw() рисует корабль на экране. И в конце метод control() сделает паузу на 17 миллисекунд. Через 17 миллисекунд run() запустится снова. И так до пока переменная gameRunning == true. Вот эти методы:
Обратите внимание на инициализацию при первом запуске. Там мы вычисляем количество пикселей в юните и добавляем корабль. Корабль мы ещё не создали. Но прежде мы создадим его родительский класс.
Шаг 7. Создаём класс SpaceBody
Он будет родительским для класса Ship (космический корабль) и Asteroid (астероид). В нём будут содержаться все переменные и методы общие для этих двух классов. Добавляем переменные:
Шаг 8. Создаём класс Ship
Теперь создадим класс Ship (космический корабль). Он наследует класс SpaceBody поэтому в определение класа добавим extends SpaceBody.
и переопределим метод update()
На этом космический корабль готов! Всё компилируем и запускаем. На экране должен появиться космический корабль. При нажатии на кнопки он должен двигаться вправо и влево. Теперь добавляем сыплющиеся сверху астероиды. При столкновении с кораблём игра заканчивается.
Шаг 9. Создаём класс Asteroid
Добавим класс Asteroid (астероид). Он тоже наследует класс SpaceBody поэтому в определение класса добавим extends SpaceBody.
Добавим нужные нам переменные:
Астероид должен появляться в случайной точке вверху экрана и лететь вниз с случайной скоростью. Для этого x и speed задаются при помощи генератора случайных чисел в его конструкторе.
Астероид должен двигаться с определённой скорость вертикально вниз. Поэтому в методе update() прибавляем к координате x скорость.
Так же нам нужен будет метод определяющий столкнулся ли астероид с кораблём.
Рассмотрим его поподробнее. Для простоты считаем корабль и астероид квадратами. Тут я пошёл от противного. То есть определяю когда квадраты НЕ пересекаются.
((x+size) (shipX+shipSize)) — корабль справа от астероида.
((y+size) (shipY+shipSize)) — корабль снизу астероида.
Между этими четырьмя выражениями стоит || (или). То есть если хоть одно выражение правдиво (а это значит что квадраты НЕ пересекаются) — результирующие тоже правдиво.
Всё это выражение я инвертирую знаком!. В результате метод возвращает true когда квадраты пересекаются. Что нам и надо.
Про определение пересечения более сложных фигур можно почитать тут.
Шаг 10. Добавляем астероиды в GameView
В GameView добавляем переменные:
также добавляем 2 метода:
И в методе run() добавляем вызовы этих методов перед вызовоом control().
Далее в методе update() добавляем цикл который перебирает все астероиды и вызывает у них метод update().
Такой же цикл добавляем и в метод draw().
Вот и всё! Простейшая 2D игра готова. Компилируем, запускаем и смотрим что получилось!
Если кто-то запутался или что-то не работает можно скачать исходник.
Игра, конечно, примитивна. Но её можно усовершенствовать, добавив новые функции. В первую очередь следует реализовать удаление вылетевших за пределы экрана астероидов. Можно сделать чтобы корабль мог стрелять в астероиды, чтобы игра постепенно ускорялась, добавить таймер, таблицу рекордов и прочее. Если это будет вам интересно — напишу продолжение, где всё это опишу.
На этом всё. Пишите отзывы, вопросы, интересующие вас темы для продолжения.
Источник