Добавим в нашу игру экшена. Как известно, в пинг-понге три действующих лица: мячик и две ракетки. Имеет смысл реализовать соответствующие классы — Ball
и Racquet
. А, поскольку имеются некоторые свойства, присущие обоим этим сущностям (как то: расположение, изображение, размеры и т.д.), то можно сделать базовый класс под названием GameObject
. Диаграмма классов будет такая:
У всех игровых объектов есть:
mPoint
— левый верхний угол прямоугольника, ограничивающего объект. С её помощью однозначно определяется положение объекта на плоскостиmImage
— изображение объекта. Изображения обычно берутся из ресурсов приложения — /res/drawablemHeight
,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() { mPoint.x += mDirection * mSpeed; // двигаем ракетку по оси Ox в соответствующую сторону } }
Отображение игровых объектов
Рисуем картинки
Ну вроде классы наши готовы, можно теперь их использовать в программе. Но сначала надо нарисовать картинки, для наших объектов. Картинки должны быть в png. У меня получилось так:
Мячик | |
Наша ракетка | |
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); }
И получаем примерно вот такую картину:
Итак
Мы реализовали игровые объекты, написали код для их перемещения. Мячик у нас успешно летает, но не отражается от стенок поля, а ракетки теоретически тоже могут перемещаться, но код для их управления еще не написан. Этими вещи мы рассмотрим в следующей статье.
По поводу отсутствующего Canvas.clear - а что, аналога FillRect там нет?
ОтветитьУдалитьУ Canvas есть метод drawRGB(r, g, b), который полностью заполняет canvas указанным цветом
ОтветитьУдалитьПри желании можно загрузить в этот Bitmap какое-нибудь изображение из ресурсов.
ОтветитьУдалитьЧто то я здесь туплю... Не могли бы вы написать код битмапа только уже с полноценной картинкой из ресурсов
2 sergey:
ОтветитьУдалитьНапример, так:
Bitmap b = BitmapFactory.decodeResource(context.getResources(), R.drawable.something);
для очискти canvas можно испльзовать
ОтветитьУдалитьcanvas.drawColor( 0, PorterDuff.Mode.CLEAR );
Спасибо за ваш блог, только начинаю разбираться в написании приложений под андроид, подписался.
ОтветитьУдалитьзамечания:
может плохо читал, но не заметил привязки скорости шарика к таймеру. Для расчета позиции шарика так и напрашивается векторная математика.
а почему sleep(20) внутри synchronized а не за ним?
ОтветитьУдалитьДаша, скажите пожалуйста, в какой программе диаграмму классов строили, судя по значкам, это эклипсовский плагин, как называется?
ОтветитьУдалитьMr. Wasserman
ОтветитьУдалитьТолько правильней запись (во всяком случае по другому у меня не работало):
canvas.drawColor( 0, Mode.CLEAR );
и импортировать
import android.graphics.PorterDuff.Mode;
Дарья, отличные статьи, спасибо!
ОтветитьУдалитьПодскажите пожалуйста - при загрузки в качестве фона битмапа, да и вообще, при перерисовке всего фона, движение шарика получается дерганым, некрасивым. Есть ли способ это забороть без применения всяких OpenGL и т.п.?
Дарья подскажите пожалуйста какой плагин для Eclipse вы используете для построения диаграммы классов?
ОтветитьУдалитьПохоже на плагин AmaterasUML
ОтветитьУдалитьВопрос от новичка:
ОтветитьУдалитьДля чего вызываем sleep() в методе run()?
Заранее благодарен.
Дарья, спасибо за статьи.