На прошлую статью про 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);
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 = 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 (в том числе при его создании) будет пересчитываться положение поля. Так что приложение будет выглядеть так:
Или так:
Итак
Мы рассмотрели необходимые понятия, написали простое приложение, которое умеет что-то рисовать, обработали поворот экрана. Исходники, как обычно, прилагаются.