Пишем игру для Android. Часть 2. Игровые объекты

 

Пишем игру для Android. Часть 2. Игровые объекты

27.03.2009

Источник: megadarja.blogspot.com

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

Ball

и

Racquet

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

GameObject

. Диаграмма классов будет такая:

 

Диаграмма классов

 

У всех игровых объектов есть:

  • mPoint

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

  • mImage

    — изображение объекта. Изображения обычно берутся из ресурсов приложения — /res/drawable

  • mHeight

    ,

    mWidth

    — размеры изображения объекта. Вынесены как поля класса только для того, чтобы не загромождать код всякими

    mImage.getIntrinsicHeight()

    и

    mImage.getIntrinsicWidth()
  • mSpeed

    — скорость перемещения объекта (фактически, на сколько перемещается объект за один шаг)

Кроме того, все игровые классы умеют рисовать себя на указанном Canvas-е (метод

draw

) и вычислять своё следующее состояние (

update

). Причем, перемещение происходит так: вычисляется следующее состояние опорной точки (

updatePoint

), а потом туда переносится изображение. А, поскольку мячик и ракетка перемещаются по-разному, метод

updatePoint

сделан абстрактным.

В класс

Ball

добавлено поле

mAngle

— угол (в градусах) к оси Ox, под которым летит мячик. В класс

Racquet

— поля

mDirection

(направление, куда движется ракетка — вправо или влево, или вообще не движется) и

mScore

(количество очков у игрока).

Реализации классов

GameObject

GameObject.java

public abstract class GameObject
{
    // Константы для направлений
    public final int DIR_LEFT = -1;
    public final int DIR_RIGHT = 1;
    public final int DIR_NONE = 0;

    /** Координаты опорной точки */
    protected Point mPoint;

    /** Высота изображения */
    protected int mHeight;

    /** Ширина изображения */
    protected int mWidth;

    /** Изображение */
    private Drawable mImage;

    /** Скорость */
    protected int mSpeed;

    /**
     * Конструктор
     * @param image Изображение, которое будет обозначать данный объект
     */
    public GameObject(Drawable image)
    {
        mImage = image;
        mPoint = new Point(0, 0);
        mWidth = image.getIntrinsicWidth();
        mHeight = image.getIntrinsicHeight();
    }

    /** Перемещение опорной точки */
    protected abstract void updatePoint();

    /** Перемещение объекта */
    public void update()
    {
        updatePoint();
        mImage.setBounds(mPoint.x, mPoint.y, mPoint.x + mWidth, mPoint.y + mHeight);
    }

    /** Отрисовка объекта */
    public void draw(Canvas canvas)
    {
        mImage.draw(canvas);
    }
    /** Задает левую границу объекта */
    public void setLeft(int value) { mPoint.x = value; }

    /** Задает правую границу объекта */
    public void setRight(int value) { mPoint.x = value - mWidth; }

    /** Задает верхнюю границу объекта */
    public void setTop(int value) { mPoint.y = value; }

    /** Задает нижнюю границу объекта */
    public void setBottom(int value) { mPoint.y = value - mHeight; }

    /** Задает абсциссу центра объекта */
    public void setCenterX(int value) { mPoint.x = value - mHeight / 2; }

    /** Задает левую ординату центра объекта */
    public void setCenterY(int value) { mPoint.y = value - mWidth / 2; }
}

Ball

Ball.java

public class Ball extends GameObject
{
    private static final int DEFAULT_SPEED = 3;
    private static final int PI = 180;

    /** Угол, который составляет направление полета шарика с осью Ox */
    private int mAngle;

    /**
     * @see com.android.pingpong.objects.GameObject#GameObject(Drawable)
     */
    public Ball(Drawable image)
    {
        super(image);

        mSpeed = DEFAULT_SPEED; // задали скорость по умолчанию
        mAngle = getRandomAngle(); // задали случайный начальный угол
    }

    /**
     * @see com.android.pingpong.objects.GameObject#updatePoint()
     */
    @Override
    protected void updatePoint()
    {
        double angle = Math.toRadians(mAngle);

        mPoint.x += (int)Math.round(mSpeed * Math.cos(angle));
        mPoint.y -= (int)Math.round(mSpeed * Math.sin(angle));
    }

    /** Генерация случайного угла в промежутке [95, 110]U[275,290] */
    private int getRandomAngle()
    {
        Random rnd = new Random(System.currentTimeMillis());

        return rnd.nextInt(1) * PI + PI / 2 + rnd.nextInt(15) + 5;
    }
}

Racquet

Racquet.java

public class Racquet extends GameObject
{
    private static final int DEFAULT_SPEED = 3;

    /** Количество заработанных очков */
    private int mScore;

    /** Направление движения */
    private int mDirection;

    /** Задание направления движения*/
    private void setDirection(int direction)
    {
        mDirection = direction;
    }

    /**
     * @see com.android.pingpong.objects.GameObject#GameObject(Drawable)
     */
    public Racquet(Drawable image)
    {
        super(image);

        mDirection = DIR_NONE; // Направление по умолчанию - нет
        mScore = 0; // Очков пока не заработали
        mSpeed = DEFAULT_SPEED; // Задали скорость по умолчанию
    }

    /**
     * @see com.android.pingpong.objects.GameObject#updatePoint()
     */
    @Override
    protected void updatePoint()
    {
        // двигаем ракетку по оси Ox в соответствующую сторону
        mPoint.x += mDirection * mSpeed;
    }
}

Отображение игровых объектов

Рисуем картинки

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

Мячик Мячик
Наша ракетка Наша ракетка
Pакетка противника Pакетка противника

Берем все это счастье, и кидаем в /res/drawable, где у нас хранятся всякие такие ресурсы.

Создаем игровые объекты

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

GameManager

. Итак, добавим в него такие поля:

GameManager.java

/** Ресурсы приложения */
private Resources mRes;

/** Мячик */
private Ball mBall;

/** Ракетка, управляемая игроком */
private Racquet mUs;

/** Ракетка, управляемая компьютером*/
private Racquet mThem;

В конструкторе инициализируем их:

GameManager.java

public GameManager(SurfaceHolder surfaceHolder, Context context)
{
    mSurfaceHolder = surfaceHolder;
    mRunning = false;

    // инициализация стилей рисования
    mPaint = new Paint();
    mPaint.setColor(Color.BLUE);
    mPaint.setStrokeWidth(2);
    mPaint.setStyle(Style.STROKE);
    Resources res = context.getResources();

    mField = new Rect();
    mBall = new Ball(res.getDrawable(R.drawable.ball));
    mUs = new Racquet(res.getDrawable(R.drawable.us));
    mThem = new Racquet(res.getDrawable(R.drawable.them));

}

Расставляем их по местам

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

initPositions()

:

GameManager.java

public void initPositions(int screenHeight, int screenWidth)
{
    int left = (screenWidth - FIELD_WIDTH) / 2;
    int top = (screenHeight - FIELD_HEIGHT) / 2;

    mField.set(left, top, left + FIELD_WIDTH, top + FIELD_HEIGHT);
    // мячик ставится в центр поля
    mBall.setCenterX(mField.centerX());
    mBall.setCenterY(mField.centerY());

    // ракетка игрока - снизу по центру
    mUs.setCenterX(mField.centerX());
    mUs.setBottom(mField.bottom);

    // ракетка компьютера - сверху по центру
    mThem.setCenterX(mField.centerX());
    mThem.setTop(mField.top);

}

Заставляем их что-то делать

Итак, объекты мы создали, теперь надо заставить их что-то делать. Понятно, что все изменения должны происходить в цикле, который работает в методе

run()

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

GameManager.java

/** Обновление объектов на экране */
private void refreshCanvas(Canvas canvas)
{
    // рисуем игровое поле
    canvas.drawRect(mField, mPaint);

    // рисуем игровые объекты
    mBall.draw(canvas);
    mUs.draw(canvas);
    mThem.draw(canvas);
}

/** Обновление состояния игровых объектов */
private void updateObjects()
{
    mBall.update();
    mUs.update();
    mThem.update();
}

А в методе

run()

будут вызовы этих методов:

GameManager.java

public void run()
{
    while (mRunning)
    {
        Canvas canvas = null;
        try
        {
            // подготовка Canvas-а
            canvas = mSurfaceHolder.lockCanvas();
            synchronized (mSurfaceHolder)
            {
                updateObjects(); // обновляем объекты
                refreshCanvas(canvas); // обновляем экран
                sleep(20);

} } catch (Exception e) { } finally { if (canvas != null) { mSurfaceHolder.unlockCanvasAndPost(canvas); } } } }

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

Бага

Делаем фон

Это всё из-за того, что мы не очищаем экран перед очередной отрисовкой. А очищать экран это тоже не так-то просто. Ничего вроде

canvas.clear()

я не нашла, так что пришлось извращаться. Суть в том, что мы заводим специальный Bitmap, при инициализации Surface задать ему размеры на весь экран, а потом в

refreshCanvas

выводить его. При желании можно загрузить в этот Bitmap какое-нибудь изображение из ресурсов.

Итак, заводим поле:

GameManager.java

/** Фон */
private Bitmap mBackground;

Инициализируем его в

initPositions

GameManager.java

public void initPositions(int screenHeight, int screenWidth)
{
    int left = (screenWidth - FIELD_WIDTH) / 2;
    int top = (screenHeight - FIELD_HEIGHT) / 2;

    mField.set(left, top, left + FIELD_WIDTH, top + FIELD_HEIGHT);
    mBackground = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.RGB_565);

// мячик ставится в центр поля mBall.setCenterX(mField.centerX()); mBall.setCenterY(mField.centerY()); // ракетка игрока - снизу по центру mUs.setCenterX(mField.centerX()); mUs.setBottom(mField.bottom); // ракетка компьютера - сверху по центру mThem.setCenterX(mField.centerX()); mThem.setTop(mField.top); }

И отрисовываем в

refreshCanvas

:

GameManager.java

private void refreshCanvas(Canvas canvas)
{
    // вывод фонового изображения
    canvas.drawBitmap(mBackground, 0, 0, null);

// рисуем игровое поле canvas.drawRect(mField, mPaint); // рисуем игровые объекты mBall.draw(canvas); mUs.draw(canvas); mThem.draw(canvas); }

И получаем примерно вот такую картину:

 

Движущийся шар

 

Итак

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

Исходники примера