Пишем свою игру для Android (Пятнашки rulez)

Вернусь к тематике игр для Android. Не для того же мы, право дело, писали здесь свой движок, чтоб вот так бесславно закончить все это крестиками-ноликами. Тут недавно почитал на каком-то форуме пост пользователя, который просит поэтапно расписать процесс создания пятнашек.

Ну вот я и подумал, что пятнашки — это дело хорошее. Пятнашки это дело нужное. Тем более, что пятнашки — это еще и дело простое и для начинающего android — программиста — самое то. 

Начать следует с изучения предметной области. Игровое поле представляет из себя квадрат 4х4 на котором расположены 15 фишек пронумерованные от 1 до 15 и одного пустого поля. Не будем изобретать велосипед и представим игровое поле в виде двумерного массива. Сделать ход можно переместив любое количество фишек в сторону пустого поля. Игрок выигрывает когда все фишки пронумерованы от 1 до 15 сверху вниз слева на право и нижний правый угол свободен.

И так, вот он класс, который реализует всю логическую составляющую игры:

package ru.davidmd.w15;

public class game15 {
	int field[][] = new int[4][4];

	/**
	 * Генерация случайного игрового поля случайным
	 * Образом
	 */
	public void GenField()
	{
		field=new int[4][4];
		for (int i = 0; i < 15; i++) {
			field[i % 4][i / 4] =  0;
		}
		for (int i = 0; i < 16; i++) {
			int x = (int)(Math.random()*4);
			int y = (int)(Math.random()*4);
			while(!(field[x][y]==0))
			{
				x = (int)(Math.random()*4);
				y = (int)(Math.random()*4);
			}

			field[x][y]=i;
		}
	}

	/**
	 * Конструктор
	 */

	public game15() {
		GenField();
	}

	/**
	 * Делает ход в зависимости от того на какое поле нажал
	 * игрок
	 * @param x координата поля по x
	 * @param y координата поля по y
	 * @return истина, если ход можно сделать
	 * ложь, если ход невозможен
	 */
	public boolean move(int x, int y) {
		// Результат изначально false
		boolean res = false;
		// Координаты пустой клетки
		int px0 = -1, py0 = -1;

		// Ищем пустую клетку на поле
		for (int i = 0; i < 4; i++) {
			for (int j = 0; j < 4; j++) {
				if (this.field[i][j] == 0) {
					px0 = i;
					py0 = j;
				}
			}
		}

		// Когда нашли делаем ход, если он возможен
		if (px0 == x || py0 == y) {
			if (!(px0 == x && py0 == y)) {
				if (px0 == x) {
					if (py0 < y) {
						for (int i = py0+1; i<= y; i++) {
							field[x][i-1] = field[x][i];
						}
					} else {
						for (int i = py0; i > y; i--) {
							field[x][i] = field[x][i - 1];
						}
					}
				}
				if (py0 == y) {
					if (px0 < x) {
						for (int i = px0+1; i <= x; i++) {
							field[i-1][y] = field[i][y];
						}
					} else {
						for (int i = px0; i > x; i--) {
							field[i][y] = field[i - 1][y];
						}
					}
				}
				field[x][y] = 0;
				res = true;
			} else {
				res = false;
			}
		}
		// Возвращаем результат
		return res;
	}

	/**
	 * Проверяет закончана ли игра
	 * @return если игра закончина возвращает true
	 * иначе возвращает false
	 */
	public boolean checkGameOver()
	{
		int a = 1;
		boolean res = true;
		for (int i = 0; i <4; i++)
			for (int j = 0; j<4; j++)
			{
				if (i==3&&j==3){a=0;}
				if (field[j][i]!=a)
				{
					res=false;
					break;
				}
				a++;
			}
		return res;
	}
}

Каждый метод достаточно подробно описан и думаю у Вас не будет проблем с тем, чтобы разобраться что тут к чему.

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

package ru.davidmd.w15;

import ru.davidmd.myengine.mScene;
import ru.davidmd.myengine.mSettings;
import ru.davidmd.myengine.mSimpleSprite;
import ru.davidmd.myengine.mSurfaceView;
import android.app.Activity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.Toast;

public class MainScreen extends Activity implements OnTouchListener {

	mSurfaceView mainview;
	mScene scene;
	mSimpleSprite sprites[] = new mSimpleSprite[15];
	game15 game;

        public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		// Начальные настройки движка
		mSettings.AutoScale = true;
		mSettings.setDefaultRes(480, 800);
		mSettings.Init(this, this.getWindowManager().getDefaultDisplay(), 20);
		mSettings.setSound(true);
		// Создаем новую игру
		game = new game15();
		// Создаем новую сцену
		scene = new mScene(480, 800, 2);
		// Устанавливаем в сцене текущим слоем первый слой (нумерация слоев с нуля)
		scene.setCurLay(1);

		// Загружем в цикле все спрайты и добавляем их на сцену
		for (int i = 0; i < 15; i++) {
			sprites[i] = new mSimpleSprite("w/w" + (i + 1) + ".png",
					this.getAssets());
			scene.addItem(sprites[i]);
		}

		// Обновляем сцену
		this.refresh();
		// Устанвливаем текущим слоем нулевой
		scene.setCurLay(0);
		// На него добавляем фоновую картинку
		scene.addItem(new mSimpleSprite("bg.png", this.getAssets()));
		// Инициализируем поверхность
		mainview = new mSurfaceView(this, scene);
		this.setContentView(this.mainview);
		mainview.setOnTouchListener(this);
	}

	/**
	 * Переводит экранные координаты x в координаты игрового поля
	 * @param x - экранная координата
	 * @return возвращает координату игрового поля соответствующую
	 *  координате x на экране
	 */
	public int scrXToGame(int x) {
		int dx = mSettings.CurrentXRes / 2 - (4 * (sprites[0].getWidth())) / 2;
		x = x - dx;
		x = x / sprites[0].getWidth();
		return x;
	}
	/**
	 * Переводит экранные координаты y в координаты игрового поля
	 * @param y - экранная координата
	 * @return возвращает координату игрового поля соответствующую
	 *  координате y на экране
	 */
	public int scrYToGame(int y) {
		int dy = (int) ((double) 40 * mSettings.ScaleFactorY);
		y = y - dy;
		y = y / sprites[0].getWidth();
		return y;
	}

	/**
	 * Обновляет положение спрайтов на экране в
	 * соответствии с игровым полем из игры
	 */
	public void refresh() {
		int dx = mSettings.CurrentXRes / 2 - (4 * (sprites[0].getWidth())) / 2;
		int dy = (int) ((double) 40 * mSettings.ScaleFactorY);
		int field[][] = game.field;
		for (int i = 0; i < 4; i++) {
			for (int j = 0; j < 4; j++) {
				if (field[i][j] != 0)
					sprites[field[i][j] - 1]
							.setXY(dx
									+ (int) (sprites[field[i][j] - 1]
											.getWidth())
									* i,
									dy
											+ (int) (sprites[field[i][j] - 1]
													.getHeight())
											* j);
			}
		}
	}

	// Обрабатываем нажатие на экран
	@Override
	public boolean onTouch(View v, MotionEvent evt) {
		if (evt.getAction() == MotionEvent.ACTION_UP) {
			scene.setCurLay(1);
			if (scene.selectSprite(evt.getX(), evt.getY()) != null) {
				if (game.move(scrXToGame((int) evt.getX()),
						scrYToGame((int) evt.getY()))) {
					this.refresh();
					if (game.checkGameOver())
					{
						Toast.makeText(this, "Вы победили!\n Ура!\n Еще" +
								"ираем?", Toast.LENGTH_LONG).show();
					}
				}
			}
		}
		return true;
	}
}

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

Собственно два эти класса, и есть полностью реализованная игра пятнашки :)!

Есть вопросы? Пишите в комментах!

UPD:

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

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

Привожу здесь код класса:

 

package ru.davidmd.w15;

public class game15 {
	int field[][] = new int[4][4];

	/**
	 * Генерация случайного игрового поля случайным
	 * Образом
	 */
//	public void GenField()
//	{
//		field=new int[4][4];
//		for (int i = 0; i < 15; i++) {
//			field[i % 4][i / 4] =  0;
//		}
//		for (int i = 0; i < 16; i++) {
//			int x = (int)(Math.random()*4);
//			int y = (int)(Math.random()*4);
//			while(!(field[x][y]==0))
//			{
//				x = (int)(Math.random()*4);
//				y = (int)(Math.random()*4);
//			}
//
//			field[x][y]=i;
//		}
//	}

	/**
	 * Навдим на поле беспорядок 🙂
	 */
	private void mixUpField()
	{
		int x,y;
		for (int i = 0 ; i<200; i++)
		{
			x = (int)(Math.random()*4);
			y = (int)(Math.random()*4);
			this.move(x, y);
		}
	}

	/**
	 * Генерим нормальное поле
	 */
	private void genNormalField()
	{
		for (int i =0; i<15; i++)
		{
			field[i%4][i/4]=i+1;
		}
	}

	/**
	 * Конструктор
	 */

	public game15() {
		genNormalField();
		mixUpField();
	}

	/**
	 * Делает ход в зависимости от того на какое поле нажал
	 * игрок
	 * @param x координата поля по x
	 * @param y координата поля по y
	 * @return истина, если ход можно сделать
	 * ложь, если ход невозможен
	 */
	public boolean move(int x, int y) {
		// Результат изначально false
		boolean res = false;
		// Координаты пустой клетки
		int px0 = -1, py0 = -1;

		// Ищем пустую клетку на поле
		for (int i = 0; i < 4; i++) {
			for (int j = 0; j < 4; j++) {
				if (this.field[i][j] == 0) {
					px0 = i;
					py0 = j;
				}
			}
		}

		// Когда нашли делаем ход, если он возможен
		if (px0 == x || py0 == y) {
			if (!(px0 == x && py0 == y)) {
				if (px0 == x) {
					if (py0 < y) {
						for (int i = py0+1; i<= y; i++) {
							field[x][i-1] = field[x][i];
						}
					} else {
						for (int i = py0; i > y; i--) {
							field[x][i] = field[x][i - 1];
						}
					}
				}
				if (py0 == y) {
					if (px0 < x) {
						for (int i = px0+1; i <= x; i++) {
							field[i-1][y] = field[i][y];
						}
					} else {
						for (int i = px0; i > x; i--) {
							field[i][y] = field[i - 1][y];
						}
					}
				}
				field[x][y] = 0;
				res = true;
			} else {
				res = false;
			}
		}
		// Возвращаем результат
		return res;
	}

	/**
	 * Проверяет закончана ли игра
	 * @return если игра закончина возвращает true
	 * иначе возвращает false
	 */
	public boolean checkGameOver()
	{
		int a = 1;
		boolean res = true;
		for (int i = 0; i <4; i++)
			for (int j = 0; j<4; j++)
			{
				if (i==3&&j==3){a=0;}
				if (field[j][i]!=a)
				{
					res=false;
					break;
				}
				a++;
			}
		return res;
	}
}

Думаю так будет гораздо интереснее 🙂

UPD: Вот готовый проект для Android mEngine

Вам понравилось? Было полезно? Поделитесь!

Опубликовать в Facebook
Опубликовать в Google Buzz
Опубликовать в Google Plus
Опубликовать в LiveJournal
Опубликовать в Мой Мир
Опубликовать в Одноклассники
Опубликовать в Яндекс
Запись опубликована в рубрике Android, Программирование с метками , , , , , , , , . Добавьте в закладки постоянную ссылку.

59 комментариев на «Пишем свою игру для Android (Пятнашки rulez)»

  1. Павел говорит:

    Проблема в том что не всегда случайно расставленную игру можно выиграть. Неплохо было бы сделать проверку на возможность выигрыша.

    • davidmd говорит:

      Хм, вообще-то хорошая идея, тока надо поискать алгоритм 🙂

      • Павел говорит:

        Когда-то я пробовал русскую IDE Профт с русским скриптовым языком. Там в примерах есть пятнашки и алгоритм проверки на выигрыш там описан был хорошо.

        • Александр говорит:

          Алгоритм я такой знаю:
          1. мысленно выстраиваем нашу доску в одну линию (пустая ячейка имеет номер 16)
          к примеру 1 5 2 4 3 (остальные отброшу для простоты)
          2. для каждого числа смотрим, сколько справа чисел, меньших его:
          для 1 — 0
          для 5 — 3
          для 2 — 0
          для 4 — 1
          для 3 — 1
          3. Суммируем получившиеся значения: 0+3+0+1+1 = 5
          4. Проверяем четность полученного значения, если четное, значит можно выиграть, иначе нет.
          Известные примеры: 1 2 3 … 14 15 16- здесь сумма равна нулю, т.к. все числа на своих местах. Ноль число четное, значит можно выиграть (что собственно уже сделано), и аналогично 1 2 3 … 13 15 14 16 — сумма перестановок равна 1, выиграть невозможно.

    • Антон говорит:

      ЛЮБУЮ случайно расставленную игру можно выиграть!

      • Александр говорит:

        Нет, не любую. Известная история — в газете была опубликована задача, в которой давались практически решенные пятнашки, за исключением последнего ряда:
        13 14 15 пусто
        Предлагался миллион долларов тому, кто поменяет 14 и 15 местами. Не выиграл никто, т.к. это невозможно.

        • Александр говорит:

          Немного напутал в предыдущем комментарии —
          13 15 14 пусто, но это не столь важно )

          • виктор говорит:

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

  2. Nata говорит:

    А вкусняшку вроде .apk как в крестиках можите скинуть??

    У вс замечательное пособие, просто и понятно, огромное вам спасибо=))

  3. Николай говорит:

    Сразу три вопроса. Как открыть в eclipse чужие проекты, я кидаю их в workspace но среда их не видит. Как подключить ваш движок к своему проекту?(извините за вопросы, но мы же новички). Третий, где на вашем сайте реклама, что бы потискать её, нам не сложно — а Вам будет приятно!!!

    • davidmd говорит:

      Ответы на первый и второй вопрос выложил здесь.
      По третьему вопросу: ну нет рекламы у меня в блоге 🙂 Хоть весь обыщите не найдете 🙂 Разве это плохо когда сайт без рекламы?
      Хотите сделать мне приятно? Скачайте игрушку и поставьте ей побольше звезд (в ней тоже нет рекламы) вам не сложно, а мне будет приятно 🙂 или запостите ссылку у себя в социалке — вам вообще один клик а мне приятно что сюда заходят новые люди и может быть узнают что-то полезное 🙂

  4. Briginas говорит:

    Есть небольшой вопрос. По поводу этого урока. Как сделать чтобы была рандомность спрайтов? Например чтобы каждый раз цифры были разного цвета. Например одного цвета хранятся в папке «w», другие в папке «t» и т.д.
    Сори если не понятно, что хочу спросить. Я просто совсем новичок во всём этом(

    • davidmd говорит:

      Думаю можно просто использовать разные спрайты. Или перезагружать спрайт.

      • Briginas говорит:

        Так я и использую разные спрайты. Спрайты одного цвета хранятся в папке «w», а другого в папке «t». Я хочу сделать так чтобы при создании Activity самой игры радномно выбиралось какого цвета будут спрайты, т.е. из какой папки их брать. Как бы правильно это прописать в коде?

        • davidmd говорит:

          Ну попробуйте например разные спрайты одного и того же объекта запихать в массив. И случайным образом выбирать элемент из этого массива.

  5. Игорь говорит:

    Прочитал все статьи про движок. Спасибо! Вот такой вопрос возник — можно ли используя Ваш движок заставить спрайт(анимированный) двигаться по какой-либо траектории? Во многих играх используется прием псевдопроизвольного движения персонажа(допустим герой которым должна управлять сама игра).

  6. Евгений говорит:

    Огромное спасибо! отличные статьи. Для начинающих то что надо. Все хорошо и доходчиво написано.
    Один вопрос: после импорта Вашего проекта эклипс ругается в классе SurfaceView на @Override перед surfaceChanged, surfaceCreated, surfaceDestroyed. Если @Override убрать то все нормально запускается. Только начинаю осваивать андроид, что я не так делаю?

    • davidmd говорит:

      Хм… Не знаю, но если у Вас все работает то и замечательно. Возможно дело в том, что у меня целая куча разных версий этого движка на разных стадиях готовности. Они очень незначительно друг от друга отличаются, и может дело именно в каких-то отличиях того что я использовал при написании и того что я выложил на сайт.

  7. Ranerg говорит:

    А не кто не продумал правильную генерацию поля?:) Нет, я не говорю что тут есть ошибки, тут есть недочеты. Пятнашки имеют !15 это порядка большое число где половина из этих этих изначальных комбинация, эта половина не имеет решения. Я сталкивался с тем что моя игра на GameMaker постоянно выдавала генерацию поля которая бы не давала решения:)

    • davidmd говорит:

      Ну здесь такого случиться не может. Собственно лень заморачиваться с проверкой, хотя наверняка существует достаточно простой алгоритм проверки.

      • Михаил говорит:

        Ну да, а вместо этого «я решил сделать несколько проще.» 😀
        Не проще ли было заглянуть в ту же Wiki за теорией, и за 5 минут решить вопрос?… там кода на 15 строчек при размашистом стиле.

  8. Владимир говорит:

    Спасибо за столь замечательные уроки, мне они очень помогли в знакомстве с разработкой под Android!
    И у меня по ходу их освоения возникло несколько вопросов:
    1) Если между перерисовками сцены, мне необходимо перерасчитывать различные параметры, например движение объектов, перезагружать спрайты, ну и вообще выполнять какие-либо действия, где их лучше выполнять (данные действия)? Правильно ли я понимаю, что надо вызывать отдельную функцию в классе mDrawerTask, методе run() перед выполнением canvas = holder.lockCanvas();? И повлияет ли на скорость работы отрисовки сцены, если все эти расчеты буду делать после canvas = holder.lockCanvas();?
    2) И еще непонятный для меня момент, при создании поверхности мы планируем задачу по таймеру, к примеру если у нас целевой FPS установлен в 25 кадров, то задача будет запускаться каждые 40 мс:
    drawer = new mDrawerTask(this.getHolder(), this.scene);
    t.scheduleAtFixedRate(drawer, 0, mSettings.frameInterval);

    Я так понимаю, что если система на андроиде будет справляться с отрисовкой сцены за 40 мс и перерасчетами перед отрисовкой, то все будет хорошо, сцена будет меняться как и запланировано. Но если этого времени не будет хватать на отрисовку, что будет происходить? полный цикл отрисовки анимированных спрайтов замедлится? Задачи будут копиться в стеке выполнения одна за другой, пока все они в конце концов не выполнятся? Если да, то что нужно предпринять чтобы этого избежать?
    Заранее благодарю за советы и подсказки 🙂

    • davidmd говорит:

      Дело в том, что отрисовка происходит вне зависимости ни от чего — в отдельном потоке. Поэтому логично различные рассчеты выносить в отдельный поток. Таким образом если вы хотите пересчитать новое положение спрайтов например и по каким-то причинам не успели, то спрайт так или иначе отрисуется на своем старом месте. Такой подход имеет как свои плюсы так и свои минусы. Но мне он кажется наиболее оправданным для игр не сильно зависящих от рассчетов.

  9. Игорь говорит:

    а как сделать самое приложение?(.apk) просто скомпилировать? Если да, то как игру сделать совместимой с Android 4.0.4?

    • davidmd говорит:

      Правой кнопкой мыши на проекте в eclipse затем выбираете Android Tools и там есть Export (Signed|Unsgned) Application Package

  10. zloilelik говорит:

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

  11. Михаил говорит:

    Скачал проект, Апк не видать, да и сам проект в эклипсе не работает.

    • davidmd говорит:

      А какая ошибка? У вас установлена необходимая версия SDK?

      • Михаил говорит:

        Eclipse, sdk все есть последней версии,кидаю проект в домашнюю папку(где проекты), хочу скомпилировать, компилит как Ant build. Скорее, может я че не правильно делаю

        • davidmd говорит:

          Я имею в виду версии установленных платформ. Попробуйте импортировать проект.

          • Михаил говорит:

            вопрос, как импортировать?? да и вопрос еще один, с помощью чего(какого элемента) можно разметить игровое поле, в котором будут двигаться кнопки?

  12. Сергей говорит:

    Eclipse не хочет компилировать. В коде находит ошибки, например, в строках x = (int)(math.random()*4);
    y = (int)(math.random()*4);
    пишет: «math cannot be resolved».
    В чем причина?

  13. dvi говорит:

    А можно выложить полный проект с движком для eclipse ? в конце статьи только движок.

  14. Иван говорит:

    Здравствуйте. Eclipse не запускает проект. Ошибочка такая error: Error: String types not allowed (at ‘layout_height’ with value ‘match_parent’), причем во всех строчках где указано ‘match_parent’. Путь mEngine/res/layout/b.xml. Заранее спасибо.

  15. Иван говорит:

    Загрузил весь SDK какой только был, но Galaxy Tab там почему-то не было. Как ни странно, проблему решило банальное удаление этого xml.

  16. Сергей говорит:

    Подскажите пожалуйста, как лечится ошибка при импорте файла. Эклипс ругается на строчку

    package ru.davidmd.w15;
    в файле game15.java

    Multiple markers at this line
    — The type java.lang.Object cannot be resolved. It is indirectly
    referenced from required .class files
    — The type java.lang.Object cannot be resolved. It is indirectly
    referenced from required .class files

    Большушее спасибо за исходники, надеюсь, разобраться как и что работает.
    Спасибо за блог!

  17. Серг говорит:

    Вопрос такой,вот в src два файла ,вот в чем вопрос,почему 2 ,какой из них главный,в общем с чего начинается ,вот такие вопросы по взаимодействию классов так сказать.Вот если не ошибаюсь в java есть класс main или процедура,который и выполняется,а здесь как MainScreen как я понял но почему?

    • davidmd говорит:

      Здесь вы указываете в файле AndroidManifest какие активити являются главными, какие могут быть вызваны системой и т.д. Тут не совсем как в Java

  18. romi говорит:

    привет, статья супер) очень хочу своему МЧ сделать сюрприз на 14 февраля, он программист. Вопрос: можно ли вместо цифр вставить его фотографии и чтобы он их собирал.. как это лучше сделать?%)

  19. Сергей говорит:

    Чего-то скрипты подозрительные на странице подвешены — очень боты подозревают их в XSS-подлянке. Бывает, что подвешивают кул-хацкеры без ведома владельца сайта. Вы уж не поленитесь проверить, а то как-то печально…

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *