В этой части мы напишем обработку выигрышей-проигрышей, реализуем подсчет очков, а также сделаем, чтобы игру можно было ставить на паузу. Собственно, пауза тут несколько не в тему, но девать ее некуда, так что сделаем ее в этой части.
Обработка проигрыша
Помнится, мы заводили в классе Racquet
поле mScore
, в котором собирались хранить количество очков у игрока. Теперь самое время начать использовать это поле.
Итак, в начале игры количество очков у обоих игроков пусто. Когда игрок не успевает отбить мяч, его противнику засчитывается очко, мячик и ракетки возвращаются на исходные позиции. Игра продолжается, пока какой-нибудь из игроков не наберет N очков. N мы пока что объявим константой, а в следующей части вынесем в настройки.
Проверка проигрыша должна осуществляться также в методе updateObjects()
GameManager
-а. Описанная нами логика запишется так:
GameManager.java
private void updateObjects() { ...// проверка проигрыша if (mBall.getBottom() < mThem.getBottom()) { mUs.incScore(); reset(); } if (mBall.getTop() > mUs.getTop()) { mThem.incScore(); reset(); }}
Racquet.incScore()
увеличивает на 1 количество очков у игрока:
Racquet.java
/** Увеличить количество очков игрока */ public void incScore() { mScore++; }
GameManager.reset()
расставляет ракетки и мячик на исходные позиции, задает мячику новый случайный угол, а также делает паузу (чтобы игрок успел понять, что произошло).
GameManager.java
private void reset() { // ставим мячик в центр mBall.setCenterX(mField.centerX()); mBall.setCenterY(mField.centerY()); // задаем ему новый случайный угол mBall.resetAngle(); // ставим ракетки в центр mUs.setCenterX(mField.centerX()); mThem.setCenterX(mField.centerX()); // делаем паузу try { sleep(LOSE_PAUSE); } catch (InterruptedException iex) { } }
LOSE_PAUSE
— это константа класса GameManager
, в которой задается длина паузы в миллисекундах (у меня она равна 2000). Метод же resetAngle()
класса Ball
выглядит следующим образом:
Ball.java
/** Задает новое случайное значение угла */ public void resetAngle() { mAngle = getRandomAngle(); }
Если теперь запустить приложение, то увидим, что, если упустить мячик, то он никуда не улетит, а через некоторое время восстановится в центре. А про очки пока ничего сказать нельзя, потому что они нигде не выводятся. Что ж, будем выводить.
Вывод количества очков
Вывод текста на экран производится с помощью метода drawText(String text, float x, float y, Paint paint)
класса Canvas. Как можно заметить, стили текста задаются с помощью экземпляра класса Paint
. Где-то в первой части мы создавали в GameManager
такое поле mPaint
, где хранились стили для рисования игрового поля. Для вывода текста можно использовать это же поле, и при каждой перерисовке экрана задавать ему стили сначала для игрового поля, а потом для текста. А можно завести отдельный экземпляр Paint
для хранения стилей текста:
GameManager.java
private Paint mScorePaint;
Инициализировать его в конструкторе:
GameManager.java
public GameManager(SurfaceHolder surfaceHolder, Context context) { mSurfaceHolder = surfaceHolder; Resources res = context.getResources(); mRunning = false; // стили для рисования игрового поля mPaint = new Paint(); mPaint.setColor(Color.BLUE); mPaint.setStrokeWidth(2); mPaint.setStyle(Style.STROKE);// стили для вывода счета mScorePaint = new Paint(); mScorePaint.setTextSize(20); mScorePaint.setStrokeWidth(1); mScorePaint.setStyle(Style.FILL); mScorePaint.setTextAlign(Paint.Align.CENTER);// игровые объекты 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)); }
А непосредственно вывод счета игры производится в методе, где происходит вся отрисовка текущей игровой ситуации — 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);// вывод счета mScorePaint.setColor(Color.RED); canvas.drawText(String.valueOf(mThem.getScore()), mField.centerX(), mField.top - 10, mScorePaint); mScorePaint.setColor(Color.GREEN); canvas.drawText(String.valueOf(mUs.getScore()), mField.centerX(), mField.bottom + 25, mScorePaint);}
Правда, совсем уж без изменения стиля не обошлось. Наши очки мы рисуем зелёным, а очки противника — красным. Запустив, увидим примерно такую картину:
Использование пользовательских шрифтов
А теперь нам захотелось использовать для вывода счета какой-нибудь наш красивый шрифт. Рассмотрим, как это можно сделать.
В нашем проекте есть такая папка assets, там хранятся такие ресурсы, как TrueType-шрифты, возможно, какие-то большие тексты и т.д.. Основное отличие их от ресурсов, которые хранятся в папке res
— это то, что используются они гораздо реже, и доставать их оттуда сложнее. Ресурсы из res
можно запросто достать с помощью класса R
, а assets вытаскиваются с помощью специального класса AssetManager
.
Итак, создадим в папке assets
папку fonts и кинем туда шрифт под названием Mini.ttf
. Теперь, чтобы достать этот шрифт и использовать его для вывода количества очков, достаточно добавить в инициализацию mScorePaint
в конструкторе одну строчку:
GameManager.java
mScorePaint.setTypeface(Typeface.createFromAsset(context.getAssets(), "fonts/Mini.ttf"));
context.getAssets()
получит менеджер ресурсов (AssetManager
) для данного приложения, откуда потом будет можно загрузить шрифт по указанному пути. Стоит обратить внимание, что путь является case-sensitive, т.е. "fonts/mini.ttf" уже ничего не загрузит.
Неприятность
И всё бы хорошо, но теперь время от времени стали возникать ситуации, когда в начале игры у одного из игроков выводится не 0 очков, а 1. Я так понимаю, что проблемы возникают в самом начале программы, перед initPositions
, когда у игровых объектов координаты еще не заданы, а updateObjects
уже вызывается. Чтобы исправить положение, заведем в классе GameManager
еще одно булево поле mInitialized
, в конструкторе зададим как false
, а в initPositions
присвоим ему true
. Тогда в run
можно написать так:
GameManager.java
public void run() { while (mRunning) { Canvas canvas = null; try { // подготовка Canvas-а canvas = mSurfaceHolder.lockCanvas(); synchronized (mSurfaceHolder) {if (mInitialized) {updateObjects(); // обновляем объекты refreshCanvas(canvas); // обновляем экран sleep(20); } } } catch (Exception e) { } finally { if (canvas != null) { mSurfaceHolder.unlockCanvasAndPost(canvas); } } } }
Теперь гарантированно не будет происходить никаких проверок, пока не будут инициализированы координаты игровых объектов. Проблема решена.
Обработка окончания игры
Прежде всего, нам следует завести в GameManager
переменную, где бы хранилось количество очков, до которого идет игра. Заведем такую переменную и сразу сеттер к ней. Итак:
GameManager.java
/** Максимальное число очков, до которого идет игра */ private static int mMaxScore = 5; public static void setMaxScore(int value) { mMaxScore = value; }
Саму проверка на окончание игры можно поместить как в метод updateObjects()
, так и прямо в run()
. Но, думаю, правильнее именно в updateObjects()
:
GameManager.java
/** Обновление состояния игровых объектов */ private void updateObjects() { // Обновление состояния игровых объектов ... // обработка столкновений ... // проверка проигрыша ...// проверка окончания игры if (mUs.getScore() == mMaxScore mThem.getScore() == mMaxScore) { this.mRunning = false; }}
Напомню, что метод run
выглядит так:
GameManager.java
public void run() { while (mRunning) { // обновление и отрисовка объектов } }
То есть, когда mRunning
станет равным false
, поток завершится. Раз он завершился — игра закончена, и надо вывести на экран ее результаты. Так что логично видеть в методе run()
что-то вроде:
GameManager.java
public void run() { while (mRunning) { // обновление и отрисовка объектов } // рисование GameOver ... }
А теперь разберемся, как это рисование может выглядеть. Как известно, при рисовании мы лочим Canvas, рисуем, и затем разлочиваем. При этом еще нужно отловить возможные исключения. Получается куча кода, которая появляется при каждом рисовании и сильно загромождает текст программы. Естественно, хочется вынести все это в метод-обертку и передавать туда ссылку на функцию, осуществляющую собственно рисование. На C# это выглядело бы так:
delegate void DrawFunction(Canvas canvas); private void draw(DrawFunction something) { Canvas canvas = null; try { canvas = mSurfaceHolder.lockCanvas(); synchronized (mSurfaceHolder) { something(canvas); } } } catch (Exception e) { } finally { if (canvas != null) { mSurfaceHolder.unlockCanvasAndPost(canvas); } } }
Но здесь нам не C#, здесь климат иной, и делегатов нет. Однако, как мне подсказал товарищ xeye, подобный код можно написать. Итак, добавим в GameManager
такой интерфейс:
GameManager.java
private interface DrawHelper { void draw(Canvas canvas); }
И такой метод, куда мы вынесем всю работу по подготовке canvas-а:
GameManager.java
private void draw(DrawHelper helper) { Canvas canvas = null; try { // подготовка Canvas-а canvas = mSurfaceHolder.lockCanvas(); synchronized (mSurfaceHolder) { if (mInitialized) { helper.draw(canvas); } } } catch (Exception e) { } finally { if (canvas != null) { mSurfaceHolder.unlockCanvasAndPost(canvas); } } }
Теперь можно завести конкретные реализации DrawHelper
на все случаи жизни. Я добавляю их две:
GameManager.java
/** Хелпер для перерисовки экрана */ private DrawHelper mDrawScreen; /** Хелпер для рисования результата игры*/ private DrawHelper mDrawGameover;
Инициализирую в конструкторе таким образом:
GameManager.java
public GameManager(SurfaceHolder surfaceHolder, Context context) { ... // функция для рисования экрана mDrawScreen = new DrawHelper() { public void draw(Canvas canvas) { refreshCanvas(canvas); } }; // функция для рисования результатов игры mDrawGameover = new DrawHelper() { public void draw(Canvas canvas) { // Вывели последнее состояние игры refreshCanvas(canvas); // смотрим, кто выиграл и выводим соответствующее сообщение String message = ""; if (mUs.getScore() > mThem.getScore()) { mScorePaint.setColor(Color.GREEN); message = "You won"; } else { mScorePaint.setColor(Color.RED); message = "You lost"; } mScorePaint.setTextSize(30); canvas.drawText(message, mField.centerX(), mField.centerY(), mScorePaint); } }; }
После этого метод run()
преображается до неузнаваемости:
GameManager.java
/** Действия, выполняемые в потоке */ public void run() { while (mRunning) { if (mInitialized) { updateObjects(); // обновляем объекты draw(mDrawScreen); } } draw(mDrawGameover); }
И сразу результат:
Пауза
Тут совсем кратко. Объявим в классе GameManager
поле:
GameManager.java
/** Стоит ли приложение на паузе */ private boolean mPaused;
Если приложение на паузе, поток работает "вхолостую", т.е. состояния объектов не меняются и вообще ничего не происходит. Это значит, в методе run()
будет следущее:
GameManager.java
public void run() { while (mRunning) {if (mPaused) continue;if (mInitialized) { updateObjects(); // обновляем объекты draw(mDrawScreen); } } draw(mDrawGameover); }
Будем ставить приложение на паузу, если нажата средняя клавиша джойстика:
GameManager.java
public boolean doKeyDown(int keyCode) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_A: mUs.setDirection(GameObject.DIR_LEFT); return true; case KeyEvent.KEYCODE_DPAD_RIGHT: case KeyEvent.KEYCODE_D: mUs.setDirection(GameObject.DIR_RIGHT); return true;case KeyEvent.KEYCODE_DPAD_CENTER: mPaused = !mPaused; draw(mDrawPause); return true;default: return false; } }
mDrawPause
— хелпер для рисования паузы. Я уже не буду приводить к нему листинг, там все просто.
Итак
У нас уже совсем готовая игра. Можно играть, выигрывать, проигрывать, ставить на паузу.
16 комментариев:
Посмотрел ряд статей по созданию пинпонга ... общее впечатление очень хорошее, давно хочу начать писать для андроида, вот только до Java всё руки не доходят.
Добавил в фэйворит, думаю пригодится
p.s ... только писать лучше YOU LOSE, а не lost, чтобы совсем по игровому, или олдскульно GAME OVER ^_^
нитку лучше ставить на паузу через wait/notify, иначе будет жрать проц потихоньку
имхо, отличия от .нет делегатов небольшие. практически, чуть менее удобный синтаксис.
public class DelegateExample {
interface MyDelegate {
void e(StringBuilder canvas);
}
void draw(MyDelegate delegate) {
StringBuilder canvas = null;
try {
canvas = new StringBuilder(1000);
canvas.append("Prepare Canvas:");
delegate.e(canvas);
System.out.println(canvas);
} catch (Exception e) {
e.printStackTrace();
} finally {
// something
}
}
public static void main(String[] args) {
DelegateExample example = new DelegateExample();
// declate delegate instance in place, through anonymous class
example.draw(new MyDelegate(){ public void e(StringBuilder canvas) {
canvas.append("Delegate1");
canvas.append("...Delegate1...");
canvas.append(">>>Delegate1<<<");
}});
//example.draw(someExternalDelegateInstance);
}
}
2 Stretch
А мне вот гуглопереводчик сказал, что надо говорить You Lost. И игрушки из детства я помню, где так писали. Хотя остатки моих знаний английского сомневаются, что это правильно.
2 xeye:
Спасибо за ликбез :) Хотя не сказала бы, что отличия небольшие. Делегат - это всего лишь ссылка на функцию, и объявляется он прямо в классе. А тут под каждый предполагаемый делегат надо городить целый интерфейс.
А вот с wait/notify у меня ничего не получилось. Я понимаю, что лучше паузить весь поток, но с наскока сделать не получилось, полезли эксепшены, и я решила отложить разбирательство с ними на потом.
http://www.exampledepot.com/egs/java.lang/PauseThread.html
в .нет объявление делегата внутри класса - это чисто синтаксис. за сценой там так же создается отдельный интерфейс. я не спорю, что так лаконичнее, но явовский способ вполне юзабелен :)
2 xeye
Да, Вы правы. Спасибо!
В порыве написал комент о том что гугл делают рус. сервисы слабже родных, удалил.
Вы видимо спрашивали у гугла "Проиграл", а попробуйте спросить "Ты проиграл"
Какой хитрый гугл :) Я пользуюсь гуглячьим джаббер-ботом, у нас с ним такой диалог состоялся:
> вы выиграли
ru2en> you have won
> ты выиграл
ru2en> you won
> вы проиграли
ru2en> you lost
> ты проиграл
ru2en> you lose
Так что на гугла надейся, а сам не плошай. lose - это инфинитив, и "you lose" - это настоящее время, т.е. "Вы проигрываете". А "you lost" - обозначение свершившегося факта, "Вы проиграли". Я бы даже You have lost написала, но как-то много букв.
В общем, надо было по-русски писать :)
Добрый день, Дарья! Я собираюсь написать свой первый вирус для Андроид-платформы. Можно я назову его в вашу честь? (в хорошем смысле конечно же, опустим тот факт, что это вирус). Я думаю, это будет наилучшим признанием вашего таланта с моей стороны. Какое название вам наиболее импонирует - "DarDroid" или скромное "MegaDasha"? С уважением, тайный поклонник.
Только если вирус хороший будет. А вообще неправославное это занятие - вирусы писать.
Опять опечатка: «Саму проверка на окончание....» проверкУ =)
Не самая лучшая идея делать проверку if(mInitialized) в главном цикле приложения. Фактически событие происходит только 1 раз, но проверка его будет происходить по 50 раз в секунду с нулевой вероятностью возникновения. В более крупных приложениях такие вещи будут хавать заметный процент ресурсов.
И ещё один косяк в листинге:
// проверка окончания игры
if (mUs.getScore() == mMaxScore mThem.getScore() == mMaxScore)
Пропущено «||» между «mUs.getScore() == mMaxScore» и «mThem.getScore() == mMaxScore».
Спасибо за хорошую статью! Скажите, как можно реализовать обработку нажатий пальцем на сенсорный экран? Я нашел небольшой пример (http://habrahabr.ru/blogs/android_development/111405/), но как его прикрутить к этой игре не знаю.
Здрасте. Понимаю что это было написано давно... но я вот тоже пытаюсь научиться писать игры..У меня проблема и пишу в надежде что может быть поможете. По вашему примеру.. я пытаюсь нарисовать тайл карту (tile map) она где то с 40-50 плиток складываеться.. Так вот каждая плитка это как 1 обьект. Если это всё впихнуть в рефреш канвас то собсно всё лагает ужасно( оно и не удивительно, столько обьектов перерисоывать) .Может быть есть какой то варинат нормально его нарисовать ?. Просто у вас здесь только 3-6 обьектов. Особо ничего не заметишь (
Отправить комментарий