Доброго дня всем!

Когда я писал эту «игру» у меня возникала масса вопросов по поводу зацикливания спрайтов так что бы они появлялись через определенное время, так же были проблемы с обнаружением столкновений двух спрайтов и более, все эти вопросы я сегодня хочу осветить в этом посте так как в интернете я не нашел нормального ответа на мои вопросы и пришлось делать самому. Пост ни на что не претендует, я новичок в разработке игр под android и пишу я для новичков в данной отрасли. Кому стало интересно прошу под кат.

Постановка задачи:


Игра должна представлять из себя поле (сцену) на котором располагается ниндзя и призраки. Нинзя должен защищать свою базу от этих призраков стреляя по ним.

Пример такой игры можно посмотреть в android market'e. Хотя я сильно замахнулся, у нас будет только похожая идея.

Вот как будет выглядеть игра:
image

Начало разработки


Создаем проект. Запускаем Eclipse — File — Android Project — Defens — Main.java.

Открываем наш файл Main.java и изменяем весь код на код который ниже:

Main.java
public class Main extends Activity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // если хотим, чтобы приложение постоянно имело портретную ориентацию
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);

        // если хотим, чтобы приложение было полноэкранным
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);

        // и без заголовка
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        
        setContentView(new GameView(this));
    }
}


Код ниже говорит нашей главной функции что запускать нужно не *.xml файл темы, а класс который у нас является самой сценой.
setContentView(new GameView(this));


Дальше Вам нужно создать класс GameView.java который будет служить для нас главным классом на котором будет производится прорисовка всех объектов. Так же в этом классе будет находится и наш поток в котором будет обрабатываться прорисовка объектов в потоке для уменьшения нагрузки игры на процессор. Вот как будет выглядеть класс когда на сцене у нас ничего не происходит:

GameView.java
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

import towe.def.GameView.GameThread;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class GameView extends SurfaceView
{
	/**Объект класса GameLoopThread*/
	private GameThread mThread;
	
	public int shotX;
    public int shotY; 
    
    /**Переменная запускающая поток рисования*/
    private boolean running = false;
	
  //-------------Start of GameThread--------------------------------------------------\\
    
	public class GameThread extends Thread
	{
		/**Объект класса*/
	    private GameView view;	 
	    
	    /**Конструктор класса*/
	    public GameThread(GameView view) 
	    {
	          this.view = view;
	    }

	    /**Задание состояния потока*/
	    public void setRunning(boolean run) 
	    {
	          running = run;
	    }

	    /** Действия, выполняемые в потоке */
	    public void run()
	    {
	        while (running)
	        {
	            Canvas canvas = null;
	            try
	            {
	                // подготовка Canvas-а
	                canvas = view.getHolder().lockCanvas();
	                synchronized (view.getHolder())
	                {
	                    // собственно рисование
	                    onDraw(canvas);
	                }
	            }
	            catch (Exception e) { }
	            finally
	            {
	                if (canvas != null)
	                {
	                	view.getHolder().unlockCanvasAndPost(canvas);
	                }
	            }
	        }
	    }
}

	//-------------End of GameThread--------------------------------------------------\\
	
	public GameView(Context context) 
	{
		super(context);
		
		mThread = new GameThread(this);
        
        /*Рисуем все наши объекты и все все все*/
        getHolder().addCallback(new SurfaceHolder.Callback() 
        {
      	  	 /*** Уничтожение области рисования */
               public void surfaceDestroyed(SurfaceHolder holder) 
               {
            	   boolean retry = true;
            	    mThread.setRunning(false);
            	    while (retry)
            	    {
            	        try
            	        {
            	            // ожидание завершение потока
            	            mThread.join();
            	            retry = false;
            	        }
            	        catch (InterruptedException e) { }
            	    }
               }

               /** Создание области рисования */
               public void surfaceCreated(SurfaceHolder holder) 
               {
            	   mThread.setRunning(true);
            	   mThread.start();
               }

               /** Изменение области рисования */
               public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) 
               {
               }
        });
	}
	
	 /**Функция рисующая все спрайты и фон*/
    protected void onDraw(Canvas canvas) {     	
          canvas.drawColor(Color.WHITE);
    }
}


Из комментариев надеюсь понятно какая функция что делает. Этот класс является базовым по этому в нем мы будем производиться все действия (функции) которые будут происходить в игре, но для начало нам нужно сделать еще несколько классов Переходи к следующему пункту — создание спрайтов.

Создание спрайтов


Спрайты это маленькие картинки в 2D-играх, которые передвигаются. Это могут быть человечки, боеприпасы или даже облака. В этой игре мы будем иметь три различных типа спрайта: Нинзя image, призрак image, и снаряд image.

Сейчас мы будем использовать не анимированные спрайты но в будущем я вставлю спрайты в проэкт, если тянет научиться делать спрайты прошу во второй урок по созданию игры под android.

Теперь загрузите эти картинки в папку res/drawable для того, чтобы Eclipse мог увидеть эти картинки и вставить в Ваш проект.

Следующий рисунок должен визуально помочь понять как будет располагаться игрок на экране.
image
Скучная картинка… Давайте лучше создадим этого самого игрока.

Нам нужно разместить спрайт на экране, как ��то сделать? Создаем класс Player.java и записываем в него следующее:

Player.java
import android.graphics.Bitmap;
import android.graphics.Canvas;

public class Player
{
        /**Объект главного класса*/
	GameView gameView;
         
        //спрайт
	Bitmap bmp;

	//х и у координаты рисунка
	int x;
	int y;

        //конструктор	
	public Player(GameView gameView, Bitmap bmp)
	{
		this.gameView = gameView;
		this.bmp = bmp;                    //возвращаем рисунок
		
		this.x = 0;                        //отступ по х нет
		this.y = gameView.getHeight() / 2; //делаем по центру
	}

	//рисуем наш спрайт
	public void onDraw(Canvas c)
	{
		c.drawBitmap(bmp, x, y, null);
	}
}


Все очень просто и понятно, наш игрок будет стоять на месте и ничего не делать, кроме как стрелять по врагу но стрельба будет реализована в классе пуля (снаряд), который будем делать дальше.

Создаем еще один файл классов и назовем его Bullet.java, этот класс будет определять координаты полета, скорость полета и другие параметры пули. И так, создали файл, и пишем в него следующее:

Bullet.java
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;

public class Bullet
{
	/**Картинка*/
    private Bitmap bmp;
    
    /**Позиция*/
    public int x;
    public int y;
    
    /**Скорость по Х=15*/
    private int mSpeed=25;
    
    public double angle;
    
    /**Ширина*/
    public int width;
    
    /**Ввыоста*/
    public  int height;
    
	public GameView gameView;
      
       /**Конструктор*/
       public Bullet(GameView gameView, Bitmap bmp) {
             this.gameView=gameView;
             this.bmp=bmp;
             
             this.x = 0;            //позиция по Х
             this.y = 120;          //позиция по У
             this.width = 27;       //ширина снаряда
             this.height = 40;      //высота снаряда
             
             //угол полета пули в зависипости от координаты косания к экрану
             angle = Math.atan((double)(y - gameView.shotY) / (x - gameView.shotX)); 
       }
 
       /**Перемещение объекта, его направление*/
       private void update() {           
    	   x += mSpeed * Math.cos(angle);         //движение по Х со скоростью mSpeed и углу заданном координатой angle
    	   y += mSpeed * Math.sin(angle);         // движение по У -//-
       }

      /**Рисуем наши спрайты*/
       public void onDraw(Canvas canvas) {
            update();                              //говорим что эту функцию нам нужно вызывать для работы класса
            canvas.drawBitmap(bmp, x, y, null);
       }
}


Из комментариев должно быть понятно что пуля выполняет только одно действие — она должна лететь по направлению указанному игроком.

Рисуем спрайты на сцене


Для того что бы нарисовать эти два класса которые мы создали, нам нужно отредактировать код в классе GameView.java, добавить несколько методов которые будут возвращать нам наши рисунки. Полностью весь код я писать не буду, буду приводить только код нужных мне методов.

Для начала нам нужно создать объекты классов Bullet и Player для того что бы отобразить их на экране, для этого создадим список пуль, что бы они у нас никогда не заканчивались, и обычный объект класса игрока.

Шапка GameView
private List<Bullet> ball = new ArrayList<Bullet>();	
private Player player;

Bitmap players;


Дальше нам нужно присвоить картинки нашим классам, находим конструктор GameView и вставляем в самый конец две строчки:

GameView.java — Конструктор GameView
players= BitmapFactory.decodeResource(getResources(), R.drawable.player2);
player= new Player(this, guns);


И в методе onDraw(Canvas c); делаем видимыми эти спрайты. Проходим по всей коллекции наших элементов сгенерировавшихся в списке.

GameView,java
 /**Функция рисующая все спрайты и фон*/
    protected void onDraw(Canvas canvas) {     	
          canvas.drawColor(Color.WHITE);
          
          Iterator<Bullet> j = ball.iterator();
          while(j.hasNext()) {
        	  Bullet b = j.next();
        	  if(b.x >= 1000 || b.x <= 1000) {
        		  b.onDraw(canvas);
        	  } else {
        		  j.remove();
        	  }
          }
          canvas.drawBitmap(guns, 5, 120, null);
    }


А для того что бы пули начали вылетать при нажатии на экран, нужно создать метод createSprites(); который будет возвращать наш спрайт.

GameView.java
 public Bullet createSprite(int resouce) {
    	 Bitmap bmp = BitmapFactory.decodeResource(getResources(), resouce);
    	 return new Bullet(this, bmp);
    }


Ну и в конце концов создаем еще один метод — onTouch(); который собственно будет отлавливать все касания по экрану и устремлять пулю в ту точку где было нажатия на экран.

GameView.java
public boolean onTouchEvent(MotionEvent e) 
    {
    	shotX = (int) e.getX();
    	shotY = (int) e.getY();
    	
    	if(e.getAction() == MotionEvent.ACTION_DOWN)
    	ball.add(createSprite(R.drawable.bullet));
    	
		return true;
    }


Если хотите сделать что бы нажатие обрабатывалось не единоразово, т.е. 1 нажатие — 1 пуля, а 1 нажатие — и пока не отпустишь оно будет стрелять, нужно удалить if(e.getAction() == MotionEvent.ACTION_DOWN) { }
и оставить только ball.add(createSprite(R.drawable.bullet));.

Все, запускаем нашу игру и пробуем стрелять. Должно выйти вот такое:


Враги


Для того что бы нам не было скучно играться, нужно создать врагов. Для этого нам придется создать еще один класс который будет называться Enemy.java и который будет уметь отображать и направлять нашего врага на нашу базу. Класс довольно простой по этому смотрим код ниже:

Enemy.java
import java.util.Random;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;

public class Enemy 
{
	/**Х и У коорданаты*/
	public int x; 
	public int y; 
	
	/**Скорость*/
	public int speed;
	
	/**Выосота и ширина спрайта*/
	public int width;
	public int height;
	
	public GameView gameView;
	public Bitmap bmp;
	
	/**Конструктор класса*/
	public Enemy(GameView gameView, Bitmap bmp){
		this.gameView = gameView;
		this.bmp = bmp;
		
		Random rnd = new Random();
		this.x = 900;
		this.y = rnd.nextInt(300);
		this.speed = rnd.nextInt(10);
		
        this.width = 9;
        this.height = 8;
	}
	
	public void update(){
		x -= speed;
	}
	
	public void onDraw(Canvas c){
		update();
		c.drawBitmap(bmp, x, y, null);
	}
}


И так что происходит в этом классе? Рассказываю: мы объявили жизненно важные переменные для нашего врага, высота ширина и координаты. Для размещения их на сцене я использовал класс Random() для того что бы когда они будут появляться на сцене, появлялись на все в одной точке, а в разных точках и на разных координатах. Скорость так же является у нас рандомной что бы каждый враг шел с разной скоростью, скорость у нас начинается с 0 и заканчивается 10, 10 — максимальная скорость которой может достигнуть враг. Двигаться они будут с права налево, для того что бы они не были сразу видны на сцене я закинул их на 900 пикселей за видимость экрана. Так что пока они дойдут можно уже будет подготовиться по полной к атаке.

Дальше нам нужно отобразить врага на сцене, для этого в классе GameView.java делаем следующее:

Создаем список врагов для того что бы они никогда не заканчивались и создаем битмап который будет содержать спрайт:

Шапка GameView
private List<Enemy> enemy = new ArrayList<Enemy>();

Bitmap enemies;


Далее создаем новый поток для задания скорости появления врагов на экране:

Шапка GameView
private Thread thred = new Thread(this);


И имплементируем класс Runuble, вот как должна выглядеть инициализация класса GameView:
public class GameView extends SurfaceView implements Runnable


Теперь у Вас еклипс требует создать метод run(), создайте его, он будет иметь следующий вид:

В самом низу класса GameView
public void run() {
		while(true) {
			Random rnd = new Random();
			try {
				Thread.sleep(rnd.nextInt(2000));  
				enemy.add(new Enemy(this, enemies));
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

Здесь мы создаем поток который будет создавать спрайт от 0 до 2000 милисекунд или каждые 0, 1 или 2 секунды.

Теперь в конструкторе в самом конце пишем инициализируем наш спрайт с классом для отображения на сцене:

Конструктор GameView
enemies = BitmapFactory.decodeResource(getResources(), R.drawable.target);       
enemy.add(new Enemy(this, enemies));


Ну и конечно же нам нужно объявить эти методы в onDraw(); Вот значит и пишем в нем следующее:

Метод onDraw() в GameView
Iterator<Enemy> i = enemy.iterator();
          while(i.hasNext()) {
        	  Enemy e = i.next();
        	  if(e.x >= 1000 || e.x <= 1000) {
        		  e.onDraw(canvas);
        	  } else {
        		  i.remove();
        	  }
          }

Снова проходим по коллекции врагов с помощью итератора и проверяем — если враг зашел за предел в 1000 пикселей — удаляем его, так как если мы не будем удалять у нас пямять закакается и телефон зависнет, а нам такие проблемы не нужны. Все игра готова для запуска.

Запускаем нашу игру и что мы увидим? А вот что:


Но что я вижу? О нет!!! Пули никак не убивают наших призраков что же делать? А я Вам скажу что делать, нам нужно создать метод который будет образовывать вокруг каждого спрайта — прямоугольник и будет сравнивать их на коллизии. Следующая тема будет об этом.

Обнаружение столкновений


И так, у нас есть спрайт, у нас есть сцена, у нас все это даже движется красиво, но какая польза от всего этого когда у нас на сцене ничего не происходит кроме хождения туда сюда этих спрайтов?

С этой функцией я навозился по полной, даже как-то так выходило что психовал и уходил гулять по улице)) Самый трудный метод, хотя выглядеть совершенно безобидно…

Ладно, давайте уже создадим этот метод и не будем много разглагольствовать… Где то в конце класса GameView создаем метод testCollision() и пишем следующий код:

В самом низу класса GameView.java
/*Проверка на столкновения*/
    private void testCollision() {
		Iterator<Bullet> b = ball.iterator();
		while(b.hasNext()) {
			Bullet balls = b.next();
			Iterator<Enemy> i = enemy.iterator();
			while(i.hasNext()) {
	        	  Enemy enemies = i.next();
	        	  
	        	 if ((Math.abs(balls.x - enemies.x) <= (balls.width + enemies.width) / 2f)
	        			 && (Math.abs(balls.y - enemies.y) <= (balls.height + enemies.height) / 2f)) {
	        			   i.remove();
	        			   b.remove();
	        	 }
			}
        }
	}


И так, что у нас происходит в этом методе? Мы создаем один итератор и запускаем цикл для просмотра всей коллекции спрайтов, и говорим что каждый следующий спрайт пули будет первым.

Дальше создаем еще один итератор с другим списком спрайтов и снова переопределяем и говорим что каждый следующий спрайт врага будет первым. И создаем оператор ветвления — if() который собственно и проверяет на столкновения наши спрайты. В нем я использовал математическую функцию модуль (abs) которая возвращает мне абсолютное целое от двух прямоугольников.

Внутри ифа происходит сравнения двух прямоугольников Модуль от (Пуля по координате Х минус координата врага по координате Х меньше либо равен ширина пули плюс ширина врага / 2 (делим на два для нахождения центра прямоугольника)) и (Модуль от (Пуля по координате У минус координата врага по координате У меньше либо равен ширина пули плюс ширина врага / 2 (делим на два для нахождения центра прямоугольника)));

И в конце всего, если пуля таки достала до врага — мы удаляем его со сцены с концами.

Ну и для того что бы эта функция стала работать записываем её в метод run() который находится в классе GameThread, ниже нашего метода рисования onDraw().

Вот что у нас получается после запуска приложения: