На прошлую статью про Android я получила хорошие отзывы, вдохновилась и решила продолжать. Эта серия статей будет посвящена написанию игры для Android.
Писать мы будем игру в пинг-понг. Изначально задумывался арканоид, но для мануала получалось слишком громоздко, так что я решила упростить до пинг-понга. Итак, есть прямоугольное поле, на нем две ракетки, управляемые игроками, и мячик. Мячик летает, отражаясь от ракеток и боковых стенок. Когда один игрок не успевает отбить мячик, его противнику засчитывается очко. Игра продолжается, пока один из игроков не наберет определенное число очков. Вот такую игру мы и будем писать. Одна ракетка будет управляться пользователем, другая — компьютером.
Создание проекта
Проект будем делать, как и в прошлый раз, в Eclipse. Создаём:
Получили автоматически сгенерившийся HelloWorld. На форме у нас единственный элемент управления — TextView
. Но нам нужно разместить на форме компонент, который бы отрисовывал игровое поле и обрабатывал нажатия клавиш. Среди стандартных такого нет, так что придется создавать свой.
Surface
Прежде, чем создавать такой класс, рассмотрим некоторые базовые понятия и классы.
SurfaceView
SurfaceView
унаследован от View
и является элементом управления, предоставляющим область для рисования (Surface). Суть в том, чтобы дать отдельному потоку возможность рисовать на Surface, когда он захочет, а не только тогда, когда приложению вздумается обновить экран. Понятие Surface очень похоже на Canvas, но все же немного не то. Canvas — это область рисования на компоненте, а Surface сам является компонентом, т.е. у Surface есть Canvas.
SurfaceView
является элементом управления, т.е. можно его непосредственно разместить на форме. Однако, в этом случае толку толку от него будет мало. Так что мы будем писать свой класс, унаследованный от SurfaceView, а также класс для потока, который будет на нем рисовать.
SurfaceHolder
Интерфейс, с помощью которого происходит вся непосредственная работа с областью рисования. Выглядит это примерно так:
SurfaceHolder surfaceHolder; ... Canvas canvas = surfaceHolder.lockCanvas(); // начали рисовать // рисуем surfaceHolder.unlockCanvasAndPost(canvas); // закончили рисовать
SurfaceHolder.Callback
Интерфейс содержит функции обработки изменения состояния Surface:
- surfaceCreated(SurfaceHolder holder) — первое создание Surface. Здесь можно, например, запускать поток, который будет рисовать на Surface.
- surfaceChanged(SurfaceHolder holder, int format, int width, int height) — любое изменение Surface (например, поворот экрана).
- surfaceDestroyed(SurfaceHolder holder) — уничтожение Surface. Здесь можно останавливать процесс, который рисует на Surface.
Класс для отображения игры
Итак, узнав, что такое Surface, можно двигаться дальше. Cоздаем класс GameView.java
, унаследованный от SurfaceView
и реализующий интерфейс SurfaceHolder.Callback
. Добавим интерфейсные функции и перегрузим конструктор. Кроме того, следует завести в этом классе ссылку на SurfaceHolder. В результате получится что-то вроде того:
GameView.java
public class GameView extends SurfaceView implements SurfaceHolder.Callback { /** * Область рисования */ private SurfaceHolder mSurfaceHolder; /** * Конструктор * @param context * @param attrs */ public GameView(Context context, AttributeSet attrs) { super(context, attrs); // подписываемся на события Surface mSurfaceHolder = getHolder(); mSurfaceHolder.addCallback(this); } @Override /** * Изменение области рисования */ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override /** * Создание области рисования */ public void surfaceCreated(SurfaceHolder holder) { } @Override /** * Уничтожение области рисования */ public void surfaceDestroyed(SurfaceHolder holder) { } }
Теперь мы можем запросто писать в разметке формы такое:
main.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" > <com.android.pingpong.GameView android:id="@+id/game" android:layout_width="fill_parent" android:layout_height="fill_parent"/> </FrameLayout>
И, запустив программу, увидим пустой экран. Теперь давайте что-нибудь нарисуем.
Класс для рисования
Поставим себе первую цель: нарисовать на экране прямоугольное поле размером 300 x 250.
Как было уже ранее сказано, все рисование должно производиться из отдельного потока. Создадим класс, GameManager
, унаследованный от Thread.
GameManager.java
public class GameManager extends Thread { private static final int FIELD_WIDTH = 300; private static final int FIELD_HEIGHT = 250; /** Область, на которой будем рисовать */ private SurfaceHolder mSurfaceHolder; /** Состояние потока (выполняется или нет. Нужно, чтобы было удобнее прибивать поток, когда потребуется) */ private boolean mRunning; /** Стили рисования */ private Paint mPaint; /** Прямоугольник игрового поля */ private Rect mField; /** * Конструктор * @param surfaceHolder Область рисования * @param context Контекст приложения */ public GameManager(SurfaceHolder surfaceHolder, Context context) { mSurfaceHolder = surfaceHolder; mRunning = false; mPaint = new Paint(); mPaint.setColor(Color.BLUE); mPaint.setStrokeWidth(2); mPaint.setStyle(Style.STROKE); int left = 10; int top = 50; mField = new Rect(left, top, left + FIELD_WIDTH, top + FIELD_HEIGHT); } /** * Задание состояния потока * @param running */ public void setRunning(boolean running) { mRunning = running; } @Override /** Действия, выполняемые в потоке */ public void run() { while (mRunning) { Canvas canvas = null; try { // подготовка Canvas-а canvas = mSurfaceHolder.lockCanvas(); synchronized (mSurfaceHolder) { // собственно рисование canvas.drawRect(mField, mPaint); } } catch (Exception e) { } finally { if (canvas != null) { mSurfaceHolder.unlockCanvasAndPost(canvas); } } } } }
Стоит отдельно упомянуть о классе Paint
. Этот класс используется для хранения всяческих используемых при рисовании стилей — цветов, толщины и стиля линий, шрифтов (это мы рассмотрим позже) и тому подобного. В остальном код достаточно прозрачен. Собственно рисование проходит всегда одинаково — лочим Canvas, рисуем, разлочиваем.
Теперь надо запустить рисовательный поток в нашем контроле. Добавляем в класс соответствущее поле:
GameView.java
/** * Поток, рисующий в области */ private GameManager mThread;
В конструкторе GameView
:
GameView.java
mThread = new GameManager(mSurfaceHolder, context);
При создании области рисования надо будет запустить наш поток:
GameView.java
public void surfaceCreated(SurfaceHolder holder) { mThread.setRunning(true); mThread.start(); }
А при удалении — прибить:
GameView.java
public void surfaceDestroyed(SurfaceHolder holder) { boolean retry = true; mThread.setRunning(false); while (retry) { try { // ожидание завершение потока mThread.join(); retry = false; } catch (InterruptedException e) { } } }
Теперь, запустив программу, видим следующее:
Поворот экрана
Как уже было упомянуто, при повороте экрана вызывается обработчик surfaceChanged
. Впрочем, при создании surface он тоже вызывается. В параметрах можно получить размеры доступной части экрана, что очень приятно, потому что с помощью класса DisplayMetrics
можно получить только полный размер экрана, куда еще входит верхнее поле, на котором рисовать нельзя).
Итак, в surfaceChanged
мы будем пересчитывать положение нашего поля на экране. Добавим в GameManager
такую функцию:
GameManager.java
/** * Инициализация положения объектов, в соответствии с размерами экрана * @param screenHeight Высота экрана * @param screenWidth Ширина экрана */ 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); }
Эта функция ставит наше игровое поле в центр экрана. Инициализацию положения mField
в конструкторе GameManager
можно вовсе убрать, оставив только:
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);mField = new Rect();}
Теперь в surfaceChanged
можно написать следующее:
GameView.java
@Override /** * Изменение области рисования */ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { mThread.initPositions(height, width); }
Теперь при изменении Surface (в том числе при его создании) будет пересчитываться положение поля. Так что приложение будет выглядеть так:
Или так:
Итак
Мы рассмотрели необходимые понятия, написали простое приложение, которое умеет что-то рисовать, обработали поворот экрана. Исходники, как обычно, прилагаются.