Пишем игру для 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);
}

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

 

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

 

Итак

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

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