воскресенье, 15 ноября 2015 г.

Спрайты и работа с ними
Пишем игру под Андроид

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

 

Комментариев нет:

Отправить комментарий