В этой части мы напишем обработку выигрышей-проигрышей, реализуем подсчет очков, а также сделаем, чтобы игру можно было ставить на паузу. Собственно, пауза тут несколько не в тему, но девать ее некуда, так что сделаем ее в этой части.
Обработка проигрыша
Помнится, мы заводили в классе 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
— хелпер для рисования паузы. Я уже не буду приводить к нему листинг, там все просто.
Итак
У нас уже совсем готовая игра. Можно играть, выигрывать, проигрывать, ставить на паузу.
Посмотрел ряд статей по созданию пинпонга ... общее впечатление очень хорошее, давно хочу начать писать для андроида, вот только до 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 обьектов. Особо ничего не заметишь (
ОтветитьУдалитьЭтот комментарий был удален автором.
ОтветитьУдалить