Пишем игру для Android. Часть 2 - Вывод рисунка на экран и его перемещение

Программирование графики в Android. CanvasКак Вы помните,первый урок цикла носил несколько обзорный характер. Мы пофилософствовали на тему аркадного игростроения, обсудили модули, из которых будет состоять наша игра, и написали небольшую заготовку, которую мы будем в дальнейшем развивать и усиливать. Если Вы по каким-то причинам пропустили этот урок, настоятельно рекомендую Вам все-таки ознакомиться с ним. Кстати, там же можно скачать исходник проекта, именно он является отправной точкой для этого урока.

Сегодня мы займемся графикой. Вывод изображения на экран телефона в Android задача почти тривиальная. Не будем ходить вокруг да около, а сразу займемся делом. Для начала выведем изображение робота в верхнем левом углу экрана. Для работы с графикой предпочтительно использовать png формат. Наш рисунок будет называтьсяdroid_1.png и иметь размер 20x20 пикселей. Для того, чтобы с рисунком стало можно работать из приложения, просто скопируйте его в папку/res/drawable-mdpi вашего проекта. Проще всего это сделать, перетащив файлик в соответствующую папку в дереве проектов в Eclipse.

Кстати, обратите внимание, в проекте существует несколько папок drawable. Дело в том, что ваша программа может запускаться на устройствах с разными размерами и разрешениями экрана, поэтому имеет смысл подготовить несколько наборов графических файлов, отличающихся размером картинок. В зависимости от того, где будет работать программа, выбирается тот или иной набор. Я выбрал папку drawable-mdpi, которая соответствует устройствам со средней плотностью точек на экране.

Откройте файл MainGamePanel.java и измените метод onDraw(Canvas canvas)

protected void onDraw(Canvas canvas){
canvas.drawBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.droid_1),10,15,null);
}

Метод drawBitmap выводит изображение droid_1.png на экран, причем левый верхний угол этого изображения помещается в точку с координатами x=10, y=15 (напоминаю, ось x направлена вправо, y - вниз, начало координат - верхний левый угол экрана). Для вывода изображения мы использовали id нашего ресурса (картинки внутри папки res), то есть droid_1. При копировании картинки в папку /res/drawable-mdpi Eclipse автоматически внес изменение в файл R.java и  имя файла стало его идентификатором. В случае, если вы скопировали файл в папку проекта не через Eclipse, а стандартными средствами вашей операционной системы, то файл R.java не изменится. Более того, если Вы раскроете папку res, то не увидите там только что скопированного файла. Чтобы обновить проект щелкните мышкой по папке res в дереве проектов в Eclipse и нажмите кнопку F5, либо выберите Refresh из контекстного меню.

Помимо метода onDraw нам понадобится также внести некоторые изменения в поток MainThread. Откройте  файл MainThread.java и измените метод run()

public void run(){
Canvas canvas;
Log.d(TAG,"Starting game loop");
while(running){
canvas=null;
// пытаемся заблокировать canvas
// для изменение картинки на поверхности
try{
canvas= this.surfaceHolder.lockCanvas();
synchronized(surfaceHolder){
// здесь будет обновляться состояние игры
// и формироваться кадр для вывода на экран
this.gamePanel.onDraw(canvas);//Вызываем метод для рисования
}
} finally{
// в случае ошибки, плоскость не перешла в
//требуемое состояние
if(canvas!=null){
surfaceHolder.unlockCanvasAndPost(canvas);
}
}
}
}
 

Как видите, мы объявили переменную canvas типа Canvas. Canvas очень распространенная в графических библиотеках концепция. Наиболее адекватно на русский язык canvas переводится как холст. Можно воспринимать этот объект, как лист бумаги, на который можно нанести рисунок. Объект Canvas содержит ряд команд, позволяющие выполнять различные операции рисования.

Мы пытаемся инициализировать эту переменную с помощью метода surfaceHolder.lockCanvas() - то есть связать переменную с активной поверхностью view. Этот код помещен в операторные скобки try {...} finally { ... }. Если внутри блока try произойдет ошибка, ну например по каким-то причинам нам не удастся заблокировать canvas под себя, то программа не завершится, а выполнится код в блоке finally. Если нам удастся получить в свое распоряжение Canvas, мы передаем его в качестве параметра методу gamePanel.onDraw, внутри которого и осуществляется рисование.

Обратите также внимание на блок synchronised. Включив этот блок в наш код, мы указываем, что получаем surfaceHolder в свое полное распоряжение, блокируем его. Это значит, что до тех пор, пока блок synchronised не закончится, никто другой параллельно не сможет работать с этим объектом.

Описанная выше логика построения программы довольно проста, тем не менее, эта заготовка может быть использована для создания подавляющего большинства аркадных игр.   Запустите программу, Вы увидите следующую картинку.

Перемещение изображения

Итак, мы научились выводить картинку на экран, давайте заставим ее двигаться. Как? Используем наши пальцы. Напишем простую реализацию drag and drop функции (кстати, более подробно об drag and drop рассказанов этой статье). Чтобы переместить рисунок, мы должны щелкнуть по нему и переместить в точку, соответствующую координатам касания. После того, как пользователь отрывает палец от экрана, координаты этой точки запоминаются и рисунок выводится туда.

Давайте создадим для нашего робота отдельный класс Droid.java, в котором определим поля для хранения координат и, собственно, рисунка.

package ru.mobilab.gamesample;
 
import android.graphics.Bitmap;
 
publicclass Droid{
 
private Bitmap bitmap;// картинка с роботом
private int x;// координата X
private int y;// координата Y
 
public Droid(Bitmap bitmap, int x, int y){
this.bitmap= bitmap;
this.x= x;
this.y= y;
}
 
public Bitmap getBitmap(){
return bitmap;
}
public void setBitmap(Bitmap bitmap){
this.bitmap= bitmap;
}
public int getX(){
return x;
}
public void setX(int x){
this.x= x;
}
public int getY(){
return y;
}
public void setY(int y){
this.y= y;
}
}

Класс очень простой. В нем определены три поля (x,y и bitmap), методы get и set, позволяющие считывать и записывать их значения, а также конструктор, внутри которого происходит начальная инициализация полей класса.

Нам также понадобятся переменные, для хранения состояния робота. В рамках решаемой в данный момент задачи робот может находиться в одном из двух состояний "перетаскивается" или "не перетаскивается". Определить эти состояния можно с помощью логической переменной touched. В момент, когда пользователь коснулся изображения робота, эта переменная должна принимать значение true. False она примет лишь в момент, когда палец будет снят с экрана.

Давайте немного доработаем класс Droid. Добавим в него метод, осуществляющий рисование draw и метод обработки касания handleActionDown.

package ru.mobilab.gamesample;
 
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.view.MotionEvent;
 
publicclass Droid{
 
private Bitmap bitmap;// картинка с роботом
private int x;// координата X
private int y;// координата Y
private boolean touched;// переменная состояния
 
public Droid(Bitmap bitmap, int x, int y){
this.bitmap= bitmap;
this.x= x;
this.y= y;
}
 
public Bitmap getBitmap(){
return bitmap;
}
public void setBitmap(Bitmap bitmap){
this.bitmap= bitmap;
}
public int getX(){
return x;
}
public void setX(int x){
this.x= x;
}
public int getY(){
return y;
}
public void setY(int y){
this.y= y;
}
 
public boolean isTouched(){
return touched;
}
 
public void setTouched(boolean touched){
this.touched= touched;
}
 
public void draw(Canvas canvas){
canvas.drawBitmap(bitmap, x-(bitmap.getWidth()/2), y-(bitmap.getHeight()/2),null);
}
 
public void handleActionDown(int eventX, int eventY){
if(eventX>=(x- bitmap.getWidth()/2)&amp;&amp;(eventX<=(x+ bitmap.getWidth()/2))){
if(eventY>=(y- bitmap.getHeight()/2)&amp;&amp;(y<=(y+ bitmap.getHeight()/2))){
// переводим робот в режим перетаскивания
setTouched(true);
}else{
setTouched(false);
}
}else{
setTouched(false);
}
 
}
}

Методы draw и handleActionDown обсудим чуть позже, а пока давайте немного поправим код в файле MainGamePanel.java.

package ru.mobilab.gamesample;
 
import android.app.Activity;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
 
publicclass MainGamePanelextends SurfaceView implements
SurfaceHolder.Callback{
 
private static final String TAG= MainGamePanel.class.getSimpleName();
 
private MainThread thread;
private Droid droid;
 
public MainGamePanel(Context context){
super(context);
// Сообщаем, что обработчик событий от поверхности будет реализован
// в этом классе.
getHolder().addCallback(this);
 
// создаем поток для игрового цикла
droid=new Droid(BitmapFactory.decodeResource(getResources(), R.drawable.droid_1),50,50);
 
// create the game loop thread
thread=new MainThread(getHolder(), this);
 
// делаем GamePanel способной обрабатывать фокус и события
setFocusable(true);
}
 
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height){
}
 
@Override
public void surfaceCreated(SurfaceHolder holder){
// В этой точке поверхность уже создана и мы можем
// безопасно запустить игровой цикл в потоке
thread.setRunning(true);
thread.start();
}
 
@Override
public void surfaceDestroyed(SurfaceHolder holder){
Log.d(TAG,"Surface is being destroyed");
//посылаем потоку команду на закрытие и дожидаемся,
//пока поток не будет закрыт.
boolean retry=true;
while(retry){
try{
thread.join();
retry=false;
} catch(InterruptedException e){
// пытаемся снова остановить поток thread
}
}
Log.d(TAG,"Thread was shut down cleanly");
}
 
@Override
public boolean onTouchEvent(MotionEvent event){
if(event.getAction()== MotionEvent.ACTION_DOWN){
// вызываем метод handleActionDown, куда передаем координаты касания
droid.handleActionDown((int)event.getX(),(int)event.getY());
 
// если щелчок по нижней области экрана, то выходим
if(event.getY()> getHeight()-50){
thread.setRunning(false);
((Activity)getContext()).finish();
}else{
Log.d(TAG,"Coords: x="+ event.getX()+",y="+ event.getY());
}
}if(event.getAction()== MotionEvent.ACTION_MOVE){
// перемещение
if(droid.isTouched()){
// робот находится в состоянии перетаскивания,
// поэтому изменяем его координаты
droid.setX((int)event.getX());
droid.setY((int)event.getY());
}
}if(event.getAction()== MotionEvent.ACTION_UP){
// Обрабатываем отпускание
if(droid.isTouched()){
droid.setTouched(false);
}
}
returntrue;
}
 
@Override
protected void onDraw(Canvas canvas){
// Заливаем canvas черным цветом
canvas.drawColor(Color.BLACK);
// Вызываем метод, который выводит рисунок робота
droid.draw(canvas);
}
}

В конструкторе класса MainGamePanel мы создали объект droid. В методе onTouchEvent обрабатывается событие касание экрана (MotionEvent.ACTION_DOWN). Если касание произошло, мы вызываем метод handleActionDown, куда передаем координаты. Приведем еще раз листинг этого метода.

public void handleActionDown(int eventX, int eventY){
if(eventX>=(x- bitmap.getWidth()/2)&amp;&amp;(eventX<=(x+ bitmap.getWidth()/2))){
if(eventY>=(y- bitmap.getHeight()/2)&amp;&amp;(y<=(y+ bitmap.getHeight()/2))){
// droid touched
setTouched(true);
}else{
setTouched(false);
}
}else{
setTouched(false);
}
 
}

Мы сравниваем координаты касания с координатами робота, и если касание происходит внутри прямоугольной области, соответствующей картинке робота, мы изменяем его состояние setTouched(true); в противном случае меняем на false.

Вернемся к методу onTouched. Помимо события MotionEvent.ACTION_DOWN, там обрабатывается событие MotionEvent.ACTION_MOVE. Оно возникает при перемещении пальца по экрану. Внутри обработчика этого события, мы проверяем состояние робота (значение поля touched) и если оно равно true  - обновляем координаты робота. В обработчике события MotionEvent.ACTION_UP мы переводим состояние робота в false.

Перейдем к методу Draw. Как помните, он вызывается у нас на каждом шаге игрового цикла. Внутри этого метода мы красим экран в черный цвет, а затем вызываем метод draw объекта droid, который выводит робота на экран.

Можете запустить проект и попытаться поперетаскивать робота по экрану.

Самостоятельное перемещение робота

Давайте теперь придадим нашему роботу самостоятельности и заставим его путешествовать по экрану без вмешательства пользователя. Робот не должен выходить за пределы экрана. Если он дошел до края - он должен изменить направление своего движения. Если наш объект движется самостоятельно - у него должна иметься какая-то скорость. Величина этой скорости влияет на изменение координат. Для понятия "скорость" мы реализуем собственный класс Speed.

Создайте класс Speed.java

package ru.mobilab.gamesample;
 
publicclass Speed{
 
private float xv=1;// составляющая скорости по оси X
private float yv=1;// составляющая скорости по оси Y
 
public Speed(){
this.xv=1;
this.yv=1;
}
 
public Speed(float xv, float yv){
this.xv= xv;
this.yv= yv;
}
 
public float getXv(){
return xv;
}
public void setXv(float xv){
this.xv= xv;
}
public float getYv(){
return yv;
}
public void setYv(float yv){
this.yv= yv;
}
 
// изменяем направление по оси X
public void toggleXDirection(){
xv=-xv;
}
 
// изменяем направление по оси Y
public void toggleYDirection(){
yv=-yv;
}
}

Робот имеет горизонтальную и вертикальную составляющую скорости. На каждом шаге цикла к текущей координате робота будим прибавлять соответствующую скорость (на самом деле в физическом смысле правильнее было бы прибавлять величину скорости, умноженную на время прошедшее с прошлой операции перемещения объекта, но мы в целях упрощения не будем так сильно заморачиваться). В конструкторе переменным vx и vy мы присвоили значение 1, то есть на каждом шаге игрового цикла робот будет смещаться на 1 пиксел. Если vx > 0 - робот движется вправо, если vx<0 - влево. Аналогично vy>0-вниз, vy<0 - вверх. В методах toggleXDirection() и toggleYDirection(), которые задают разворот направления движения по соответствующему направлению, мы просто умножаем vx или vy на -1.

Изменять координаты робота нужно на каждом шаге игрового цикла. Откройте файл MainThread.java и добавьте в метод run() строку

this.gamePanel.update();

метод примет вид

public void run(){
Canvas canvas;
Log.d(TAG,"Starting game loop");
while(running){
canvas=null;
// пытаемся заблокировать canvas
// для изменение картинки на поверхности
try{
canvas= this.surfaceHolder.lockCanvas();
synchronized(surfaceHolder){
// обновляем состояние
this.gamePanel.update();
// формируем новый кадр
this.gamePanel.onDraw(canvas);//Вызываем метод для рисования
}
} finally{
// в случае ошибки, плоскость не перешла в
//требуемое состояние
if(canvas!=null){
surfaceHolder.unlockCanvasAndPost(canvas);
}
}
}
}

Создадим соответствующий метод MainGamePanel. Внутри метода update() мы будем менять состояние всех игровых объектов. Пока у нас только один объект - робот, но скоро их станет больше. Внутри этого метода мы должны проверить, не столкнулся ли робот со стеной. Если столкнулся – надо поменять направление движения. При вычислении столкновений нужно помнить, что координата робота соответствует центру рисунка, поэтому чтобы получить координату правого края мы должны вычесть из координаты x половину ширины рисунка. После проверки на столкновения мы можем прибавить к координатам робота соответствующие приращения скорости, для решения этой задачи напишем метод update в классе Droid. Перейдем к файлу MainGamePanel.java

public void update(){
// проверяем столкновение с правой стеной
if(droid.getX()+ droid.getBitmap().getWidth()/2>= getWidth()){
droid.getSpeed().toggleXDirection();
}
// проверяем столкновение с левой стеной
if( droid.getX()- droid.getBitmap().getWidth()/2<=0){
droid.getSpeed().toggleXDirection();
}
// проверяем столкновение с нижней стеной
if(droid.getY()+ droid.getBitmap().getHeight()/2>= getHeight()){
droid.getSpeed().toggleYDirection();
}
// проверяем столкновение с верхней стеной
if(droid.getY()- droid.getBitmap().getHeight()/2<=0){
droid.getSpeed().toggleYDirection();
}
// Обновляем координаты робота
droid.update();
}

Методы getWidth() и getHeight() возвращают ширину и высоту рисунка.

Последняя строка метода вызывает метод update объекта droid. Добавим этот метод в класс Droid.

public void update(){
if(!touched){
x=(int)(x+speed.getXv());
y=(int)(y+speed.getYv());
}
}

Добавим в класс Droid поле

private Speed speed;

В конструкторе создадим объект speed

public Droid(Bitmap bitmap, int x, int y){
this.bitmap= bitmap;
this.x= x;
this.y= y;
speed=new Speed();
}

 и добавим метод getSpeed(), возвращающий объект speed.

public Speed getSpeed(){
return speed;
}

Все. Можно сохранять проект и пробовать его запустить. Если Вы все сделали правильно, робот будет двигаться по экрану, отскакивая от краев. Кстати, робота по-прежнему можно перетаскивать.  Исходный код рабочего примера можноскачать здесь.

Перевод:Александр Ледков

Источники: Android Game Development - Displaying Images with Android, Android Game Development - Moving Images on Screen




Наши соцсети

Подписаться Facebook Подписаться Вконтакте Подписаться Twitter Подписаться Google Подписаться Telegram

Популярное

Ссылки

Новости [1] [2] [3]... Android/ iOS/ J2ME[1] [2] [3]) Android / Архив

Рейтинг@Mail.ru Яндекс.Метрика
MobiLab.ru © 2005-2018
При использовании материалов сайта ссылка на www.mobilab.ru обязательна