Спрайты и работа с ними
Пишем игру под Андроид
Часто
в игре нужно создать объект, который не только перемещается по экрану, но и сам
видоизменяется, например, шагающий человек или облако взрыва. Как правило, это
делается с помощью покадровой анимации. Суть метода в том, что быстро
показываются кадры меняющегося объекта, в результате чего наблюдатель не видит
отдельные кадры, только иллюзию меняющегося объекта. Изображения мы сохраняем в
формате png
с прозрачной основой, что позволяет накладывать изображение объекта на фоновую
основу, например, олененок (объект) скачет по дороге (фон).
В
нашей игре сначала мы создадим иллюзию вращающегося падающего астероида, а
потом анимацию взрыва после удара по базе.
Сейчас
у нас астероид это кадр с изображением астероида, который мы перемещаем по
экрану, создавая иллюзию падения.
Чтобы
создать иллюзию вращения астероида, необходимо иметь несколько десятков кадров
астероида, на которых он нам будет виден в разных положениях в момент вращения.
Если вы хороший художник, вы можете их нарисовать, но я использую программу Blender, в которой создаю 3D модель астероида. В той же программе,
получаю изображения астероида для разных углов поворота. Возникает вопрос, а
сколько нужно изображений, чтобы на экране устройства это выглядело не только
красиво, но и функционально. Нужно помнить, что игра у нас происходит в потоке.
Мы настроили поток таким образом, что на прорисовку и отображение на экране
кадра у нас уходит 0,04с (25 кадров/с), т.е. 25 кадров астероида пробегут перед
глазами за 1 с. Если астероид делает 2 оборота в секунду, вам необходимо один
оборот отобразить в 12 кадрах. Давайте в нашей игре сделаем анимацию вращения с
помощью 35 кадров астероида. На каждом последующем кадре астероид будет
повернут на 10 градусов. 36 кадр нам не понадобится, так как он полностью будет
совпадать с первым.
Теперь
давайте рассмотрим технологию отображения кадров. Казалось бы, что просто 35
кадров нужно разместить в папку ресурсов и программно вызывать нужный нам кадр,
но есть решение, которое позволяет нам для начала существенно уменьшить
занимаемый объем памяти устройства. Дело в том, что когда вы создаете файл в
формате png,
кроме самого изображения в нем передается много вспомогательной информации,
например, о настройке цветов, прозрачности, размере, дате создания и т.д. И эту
информацию вы будете загружать в память для каждого файла. Было решено создать
один файл, в котором разместить все 35 изображений нашей анимации вращения
астероида. Такой файл назвали спрайтом.
Здесь можно
скачать этот спрайт
Теперь
мы загрузим информацию о файле только один раз. Есть специальные программы,
которые позволяют «склеивать» ваши 35 файлов в один спрайт.
Скачать
такую программу можно здесь
Итак,
у нас есть спрайт. Теперь нужно программно организовать показ нужного
изображения из спрайта.
Сначала
загрузите файл спрайта в папку ресурсов res -> drawable.
Я
назвал его sprite_asteroid.
Потом
нам нужно в джава-классе GameView создать
переменную с ссылкой на данный ресурс.
mSprite_Asteroid = BitmapFactory.decodeResource(getResources(), R.drawable.sprite_asteroid);
В методе public void render(Canvas canvas)запишем такой код.
//Work with sprite_asteroid
//В первой строчке мы фактически разрезаем спрайт на 35 частей и определяем ширину одного кадра widthSprite_Asteroid = mSprite_Asteroid.getWidth() / 35;
//потом определяем высоту кадра heightSprite_Asteroid = mSprite_Asteroid.getHeight();
//вводим переменные, которые помогут нам выбрать нужный кадр из спрайта int srcX1 = currentFrame1 * widthSprite_Asteroid; int srcY1 = heightSprite_Asteroid;
//вырезаем нужный кадр Rect src1 = new Rect(srcX1, 0, srcX1 + widthSprite_Asteroid, heightSprite_Asteroid); //вводим масштабирование, так как на разных экранах наш астероид должен выглядеть по разному. scale_spriteAsteroid = width / 5;
задаем область прямоугольника, в которую выведем картинку астероида //Rect(int left, int top, int right, int bottom) Rect dst1 = new Rect(xSpriteAsteroid, ySpriteAsteroid, xSpriteAsteroid + scale_spriteAsteroid, ySpriteAsteroid + scale_spriteAsteroid);
//определяем текущий кадр.
// % - модуль числа, если есть остаток при делении, то будет 1, нет – будет ноль. Таким образом переменная currentFrame1 принимает значения от 1 до 35. currentFrame1 = ++currentFrame1 % 35;
//выводим вырезанную картинку из спрайта в область прямоугольника на экране. canvas.drawBitmap(mSprite_Asteroid, src1, dst1, null);
Потом в методе public void update()
зададим движение спрайта.
//move SpriteAsteroid
//Если спрайт вышел за поле экрана по высоте if (ySpriteAsteroid > height) {
//назначаем новую координату по ОУ и по случайному закону координату по ОХ ySpriteAsteroid = -50; Random rnd1 = new Random(); xSpriteAsteroid = rnd1.nextInt(width - 2 * widthSprite_Asteroid);
} else {
//если не вышел из зоны видимости, то координата по У растет при каждом цикле на величину height /150 ySpriteAsteroid = ySpriteAsteroid + height /150; }
Очень важно, приращение по У делать с учетом размеров экрана девайса.
Хочу заметить, что именно по такому принципу я организовал спрайты
в своей игре Animal Zoo
https://play.google.com/store/apps/details?id=com.adc2017gmail.az01&hl=ru
Теперь нужно организовать столкновение с базой. Как это делать, описано в прошлом уроке.
Осталось вставить спрайт взрыва в момент столкновения. Спрайт взрыва
организован также, как и спрайт астероида, но чтобы он проигрывался один раз
введена булева переменная explos. Если explos==true, то спрайт проигрывается.
Когда проигрываем все 12 кадров спрайта взрыва переменная становится равной
false.
Привожу файл GameView на данный момент, остальные файлы не изменились.
package com.adc2017gmail.moonbase; import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.preference.PreferenceManager;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.util.Random; public class GameView extends SurfaceView implements SurfaceHolder.Callback { private final Drawable mAsteroid; private final Drawable mBackground; private final Bitmap mSprite_Asteroid; private Bitmap mExsplosion; private int widthAsteroid; private int heightAsteroid; private int leftAsteroid; private int rightAsteroid; private int topAsteroid; private int yAsteroid = -30; private int bottomAsteroid; private int centerAsteroid; private int height; private int width; private int speedAsteroid = 5; private int xAsteroid = 30; private MainThread thread; private String ListPreference; private int widthSprite_Asteroid; private int heightSprite_Asteroid; private int currentFrame1; private int scale_spriteAsteroid; private int xSpriteAsteroid = 100; private int ySpriteAsteroid = -50; private boolean explos; private int widthSprite; private long heightSprite; private int currentFrame =0; private int yExplosion; private int xExplosion; public GameView(Context context) { super(context); // adding the callback (this) to the surface holder to intercept events getHolder().addCallback(this); // create mAsteroid where adress picture asteroid mAsteroid = context.getResources().getDrawable(R.drawable.asteroid); mSprite_Asteroid = BitmapFactory.decodeResource(getResources(),
R.drawable.sprite_asteroid); mExsplosion = BitmapFactory.decodeResource(getResources(),
R.drawable.explosion_sprite); getPrefs(); if(ListPreference.equals("1")){ mBackground = context.getResources().getDrawable(R.drawable.cosmos1); } else{ mBackground = context.getResources().getDrawable(R.drawable.cosmos2); } // create the game loop thread thread = new MainThread(getHolder(), this); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width,int height) { } @Override public void surfaceCreated(SurfaceHolder holder) { thread.setRunning(true); thread.start(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { thread.setRunning(false); boolean retry = true; while (retry) { try { thread.join(); retry = false; } catch (InterruptedException e) { // try again shutting down the thread } } } public void render(Canvas canvas) { height = canvas.getHeight(); width = canvas.getWidth(); //set the background mBackground.setBounds(0,0, width, height); mBackground.draw(canvas); //Work with asteroid widthAsteroid = 2 * width / 13;//set width asteroid heightAsteroid = widthAsteroid; leftAsteroid = xAsteroid;//the left edge of asteroid rightAsteroid = leftAsteroid + widthAsteroid;//set right edge of asteroid topAsteroid = yAsteroid; bottomAsteroid = topAsteroid + heightAsteroid; centerAsteroid = leftAsteroid + widthAsteroid / 2; mAsteroid.setBounds(leftAsteroid, topAsteroid, rightAsteroid, bottomAsteroid); mAsteroid.draw(canvas); //Work with sprite_asteroid widthSprite_Asteroid = mSprite_Asteroid.getWidth() / 35; heightSprite_Asteroid = mSprite_Asteroid.getHeight(); int srcX1 = currentFrame1 * widthSprite_Asteroid; int srcY1 = heightSprite_Asteroid; Rect src1 = new Rect(srcX1, 0, srcX1 + widthSprite_Asteroid,
heightSprite_Asteroid); scale_spriteAsteroid = width / 5; //Rect(int left, int top, int right, int bottom) Rect dst1 = new Rect(xSpriteAsteroid, ySpriteAsteroid, xSpriteAsteroid + scale_spriteAsteroid, ySpriteAsteroid + scale_spriteAsteroid); currentFrame1 = ++currentFrame1 % 35; canvas.drawBitmap(mSprite_Asteroid, src1, dst1, null); //animation explosion if(explos==true && currentFrame < 13) { widthSprite = mExsplosion.getWidth() / 12; heightSprite = mExsplosion.getHeight(); int srcX = currentFrame * widthSprite; int srcY = (int) heightSprite; Rect src = new Rect(srcX, 0, srcX + widthSprite, (int) heightSprite); Rect dst = new Rect(xExplosion-scale_spriteAsteroid,
yExplosion-scale_spriteAsteroid, (int) (xExplosion + 2*scale_spriteAsteroid),
yExplosion+ 2* scale_spriteAsteroid); currentFrame++; canvas.drawBitmap(mExsplosion, src, dst, null); } else { currentFrame = 0; explos = false; } } public void update() { //move Asteroid if (yAsteroid > 8*height/10&& rightAsteroid <= width/4|| yAsteroid > 7*height/10&& rightAsteroid > width/4&& leftAsteroid<=3*width/4|| yAsteroid > 8*height/10&& leftAsteroid > 3*width/4 ) { yAsteroid = -height/2; // find by random function Asteroid & speed Asteroid Random rnd = new Random(); xAsteroid = rnd.nextInt(width - widthAsteroid); speedAsteroid = 5+ rnd.nextInt(10); } else { yAsteroid +=speedAsteroid; } //move SpriteAsteroid if (ySpriteAsteroid > height) { ySpriteAsteroid = -50; Random rnd1 = new Random(); xSpriteAsteroid = rnd1.nextInt(width - 2 * widthSprite_Asteroid); } else { ySpriteAsteroid = ySpriteAsteroid + height /150; } //Terms collision with sprite_asteroid if (ySpriteAsteroid > 8*height/10&& ySpriteAsteroid+scale_spriteAsteroid <= width/4|| ySpriteAsteroid> 7*height/10&&ySpriteAsteroid+scale_spriteAsteroid > width/4
&& xSpriteAsteroid<=3*width/4|| ySpriteAsteroid > 8*height/10&& xSpriteAsteroid > 3*width/4 ) { yExplosion = ySpriteAsteroid; xExplosion = xSpriteAsteroid; ySpriteAsteroid = -height; Random rnd2 = new Random(); xSpriteAsteroid = rnd2.nextInt(width - 2 * widthSprite_Asteroid); explos = true; } else { ySpriteAsteroid = ySpriteAsteroid + height /150; } } private void getPrefs() { // Get the xml/preferences.xml preferences SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); ListPreference = prefs.getString("background", "1"); } }
Видео того, что получилось можно посмотреть здесь.
https://youtu.be/856QXc7D9rw
На следующем уроке вставим звуки, а то как-то тихо всё у нас. J
Комментариев нет:
Отправить комментарий