Спрайты и работа с ними
Пишем игру под Андроид
Часто в игре нужно создать объект, который не только перемещается по экрану, но и сам видоизменяется, например, шагающий человек или облако взрыва. Как правило, это делается с помощью покадровой анимации. Суть метода в том, что быстро показываются кадры меняющегося объекта, в результате чего наблюдатель не видит отдельные кадры, только иллюзию меняющегося объекта. Изображения мы сохраняем в формате 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
Комментариев нет:
Отправить комментарий