Пишем змейку для Android (Часть 1)

Всем привет! Сегодня мы начнем писать змейку для Android.

После достаточно долгого перерыва решил написать еще парочку статей. Дело в том, что владелец одного из сайтов смежной тематики предложил мне написать пару статей для его ресурса. Статьи должны были быть на тему создания какойнить простой игры. Я подумал и решил сделать змейку. Игра простая, но достаточно любопытная! Правда потом адрес владельца этого ресурса у меня куда-то потерялся, а вот сама игрушка осталась. Решил собственно осветить процесс. Игра написана без применения моего движка, так что эту серию статеек можно читать в отрыве от всего остального!

Ну, приступим! Для начала давайте разберемся что такое змейка, в чем смысл этой игры и пр. Значит по экрану устройства бегает изгибающаяся колбаса (собственно змейка!) кушает некие объекты на экране, съев объект начинает расти не по дням а по часам, столкнувшись сама с собой или с каким-то препятствием змея благополучно умирает :(. Возможны различные уровни сложности (ускоряется сама змея или появляется больше препятствий на ее пути). Управлять этой штукой я думаю лучше всего посредством акселерометра.

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

Начнем пожалуй с реализации логики самого приложения. Для этого опишем вот такой класс SnakeGame- который есть ни что иное, как реализация логики самой игры.

package ru.davidmd.simpleSnake;

import java.util.ArrayList;

import android.content.Context;

public class SnakeGame {

	// класс определяющий позицию
	public class pos {
		int x;
		int y;

		//конструктор
		pos(int x, int y) {
			this.x = x;
			this.y = y;
		}
	}

	// Константы направления
	public static final int DIR_NORTH = 1;
	public static final int DIR_EAST = 2;
	public static final int DIR_SOUTH = 3;
	public static final int DIR_WEST = 4;

	// ширина и высота игрового поля
	// подбиралось исходя из пропорций экрана моего девайса
	public static int mFieldX = 18;
	public static int mFieldY = 30;

	// Очки в игре
	public int mScore=0;

	// Матрица - игровое поле
	private int mField[][] = new int[mFieldX][mFieldY];

	// Сама змея - массив двумерных координат каждого сегмента
	private ArrayList<pos> mSnake = new ArrayList<pos>();

	// Текущее напраление движения змеи
	int mDirection = SnakeGame.DIR_EAST;

	// пераметр по которому определяется должна ли
	// змейка расти или нет смотрите ниже там понятно
	int isGrowing = 0;

	// Собственно конструктор
	SnakeGame() {
		// очищаем игровое поле
		for (int i = 0; i < mFieldX; i++)
			for (int j = 0; j < mFieldY; j++) {
				mField[i][j] = 0;
			}
		// создаем змею
		mSnake.add(new pos(2, 2));
		// каждая клетка поля в которой
		// находится змея - отмечается -1
		mField[2][2] = -1;
		mSnake.add(new pos(2, 3));
		mField[2][3] = -1;
		mSnake.add(new pos(2, 4));
		mField[2][4] = -1;
		// добавляем на игровое поле фрукт
		addFruite();
	}

	// метод добавляет фрукт на игровое поле
	// поскольку игра у нас самая простая то
	// и фрукт только один а его код на поле - 2
	private void addFruite() {
		boolean par = false;
		while (!par) {
			int x = (int) (Math.random() * mFieldX);
			int y = (int) (Math.random() * mFieldY);
			if (mField[x][y] == 0) {
				mField[x][y] = 2;
				par = true;
			}
		}
	}

	// Этот метод соержит в себе всю логику игры
	// здесь опиываются все действия которые должны происходить
	// при каждом перемещении змеи
	// при этом, учитывается текущее направление и
	// проверяется, может ли змея ходить в указанном направлении
	// собственно вся игровая логика заключена в этом методе
	public boolean nextMove() {
		// смотрим, куда у нас направлена змея сейчас
		switch (this.mDirection) {
		// если на север
		case DIR_NORTH: {
			// тогда рассчитываем координаты в которые попадет
			// голова змеи на следуюзщем ходу
			int nextX = mSnake.get(mSnake.size() - 1).x;
			int nextY = mSnake.get(mSnake.size() - 1).y - 1;
			// если мы не утыкаемся в верхнюю стенку
			// и если клетка куда мы идем пуста (о чем нам говорит
			// нулевое значение в указанной клетке поля)
			if ((nextY >= 0) && mField[nextX][nextY] == 0) {
				// то мы проверяем, растет ли в данный момент змея
				if (isGrowing > 0) {
					// если растет, уменьшаем запас роста и не
					// двигаем хвост змеи
					isGrowing--;
				} else {
					// если не растет, то передвигаем хвост змеи
					mField[mSnake.get(0).x][mSnake.get(0).y] = 0;
					mSnake.remove(0);
				}
				//Затем перемещаем голову змеи
				mSnake.add(new pos(nextX, nextY));
				mField[nextX][nextY] = -1;
				// и на этом все закончилось 🙂 возвращаем истину
				return true;
			} else if ((nextY >= 0) && mField[nextX][nextY] == 1) {
				// если мы уткнулись в препятствие возвращаем ложь
				return false;
			} else if ((nextY >= 0) && mField[nextX][nextY] > 1) {
				// А вот если мы уткнулись во фрукт,
				// тогда увеличиваем запас роста
				isGrowing = isGrowing + 2;
				// добавляем очков!
				mScore=mScore+10;
				// и переносим голову змеи
				// на соответствующую клетку поля
				mField[nextX][nextY] = 0;
				mSnake.add(new pos(nextX, nextY));
				mField[nextX][nextY] = -1;
				// ну и соответственно добавляем на поле новый фрукт!
				addFruite();
				return true;
			} else {
				// во всех остальных случаях возвращаем false
				return false;
			}
		}
		// Здесь все то же самое, только
		// для других направлений
		case DIR_EAST: {
			int nextX = mSnake.get(mSnake.size() - 1).x + 1;
			int nextY = mSnake.get(mSnake.size() - 1).y;
			if ((nextX < mFieldX) && mField[nextX][nextY] == 0) {
				if (isGrowing > 0) {
					isGrowing--;
				} else {
					mField[mSnake.get(0).x][mSnake.get(0).y] = 0;
					mSnake.remove(0);
				}
				mSnake.add(new pos(nextX, nextY));
				mField[nextX][nextY] = -1;
				return true;
			} else if ((nextX < mFieldX) && mField[nextX][nextY] == 1) {
				return false;
			} else if ((nextX < mFieldX) && mField[nextX][nextY] > 1) {
				isGrowing = isGrowing + 2;
				mScore=mScore+10;
				mField[nextX][nextY] = 0;
				mSnake.add(new pos(nextX, nextY));
				mField[nextX][nextY] = -1;
				addFruite();
				return true;
			} else {
				return false;
			}
		}
		case DIR_SOUTH: {
			int nextX = mSnake.get(mSnake.size() - 1).x;
			int nextY = mSnake.get(mSnake.size() - 1).y + 1;
			if ((nextX < mFieldX) && mField[nextX][nextY] == 0) {
				if (isGrowing > 0) {
					isGrowing--;
				} else {
					mField[mSnake.get(0).x][mSnake.get(0).y] = 0;
					mSnake.remove(0);
				}
				mSnake.add(new pos(nextX, nextY));
				mField[nextX][nextY] = -1;
				return true;
			} else if ((nextX < mFieldX) && mField[nextX][nextY] == 1) {
				return false;
			} else if ((nextX < mFieldX) && mField[nextX][nextY] > 1) {
				isGrowing = isGrowing + 2;
				mScore=mScore+10;
				mField[nextX][nextY] = 0;
				mSnake.add(new pos(nextX, nextY));
				mField[nextX][nextY] = -1;
				addFruite();
				return true;
			} else {
				return false;
			}
		}
		case DIR_WEST: {
			int nextX = mSnake.get(mSnake.size() - 1).x - 1;
			int nextY = mSnake.get(mSnake.size() - 1).y;
			if ((nextX >= 0) && mField[nextX][nextY] == 0) {
				if (isGrowing > 0) {
					isGrowing--;
				} else {
					mField[mSnake.get(0).x][mSnake.get(0).y] = 0;
					mSnake.remove(0);
				}
				mSnake.add(new pos(nextX, nextY));
				mField[nextX][nextY] = -1;
				return true;
			} else if ((nextX >= 0) && mField[nextX][nextY] == 1) {
				return false;
			} else if ((nextX >= 0) && mField[nextX][nextY] > 1) {
				isGrowing = isGrowing + 2;
				mScore=mScore+10;
				mField[nextX][nextY] = 0;
				mSnake.add(new pos(nextX, nextY));
				mField[nextX][nextY] = -1;
				addFruite();
				return true;
			} else {
				return false;
			}
		}
		}
		return false;
	}

	// здесь и нижу всякие геттеры и сеттеры
	// думаю тут и объяснять нечего
	public int getDirection() {
		return mDirection;
	}

	public void clearScore(){
		this.mScore=0;
	}

	public void setDirection(int direction) {
		this.mDirection = direction;
	}

	public int[][] getmField() {
		return mField;
	}

	public int getSnakeLength() {
		return  mSnake.size();
	}

	public ArrayList<pos> getmSnake() {
		return mSnake;
	}
}

Что мы тут имеем? Начнем с вложенного класса pos. Это просто позиция — два целых числа x и y. Затем имеются константы направлений — ну и собственно сама переменная, в которой хранится направление — mDirection. А самое главное здесь у нас имеется два основных компонента змейки. Перовое (сама змейка) — массив mSnake двумерные координаты каждого сегмента змейки. Второе — само игровое поле mField — двумерный массив, каждый элемент массива кодирует одну клетку игрового поля: -1 это клетка в которой находится змея; 0 — это клетка в которой ничего нет, а 2 — это клетка в которой есть фрукт. Есть так же возможность использовать код 1 — в качестве стенки, но это так, задел на будущее :-).

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

По сути в том классе есть только один достаточно большой метод, разобраться с которым надо поподробнее. Это nextMove() — возвращающий true — если змея может двигаться дальше в направлении указанном в переменной mDirection. Прежде всего определяется в каком именно направлении должна двигаться змейка, затем для каждого направления проверяются такие параметры как «не упрется ли змея в стену», «не съест ли она фрукт», и если съест, то как именно будет расти дальше. В случае, если все проверки прошли удачно, то возвращается истина, если нет, то ложь. В коде достаточно подробно откомментировано каждое действие. Думаю не составит особого труда разобраться что к чему.

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

Итак, для этой activity я сделал два разных файла разметки. Первый файл содержит всего одну кнопку, а второй — надписи и кнопку. Вот скриншоты:

А вот сам код класса:

package ru.davidmd.simpleSnake;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;

public class SimpleSnakeActivity extends Activity implements OnClickListener {

	Button butt;
	TextView tv;

	// режим запуска активити - 0 первый запуск
	// 1 - запуск активити после проигрыша
	public static int GAME_MODE=0;

	public static int GAME_SCORE=0;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
	}

	@Override
	public void onStart(){
		super.onStart();
		// Вот тут забавный момент с
		// загрузкой разных разметок
		if (GAME_MODE==0){
		setContentView(R.layout.main);
		butt = (Button) this.findViewById(R.id.button1);
		butt.setOnClickListener(this);
		}
		else
		{
			setContentView(R.layout.lose);
			butt = (Button) this.findViewById(R.id.button2);
			tv = (TextView) this.findViewById(R.id.textView2);
			tv.setText("Your score: "+GAME_SCORE);
			butt.setOnClickListener(this);
		}
	}

	public void onClick(View v) {
		// Для любой разметки если мы нажимем на кнопку, то игра запускается
		Intent i = new Intent(this, ru.davidmd.simpleSnake.GameActivity.class);
		GAME_MODE=0;
		GAME_SCORE=0;
		this.startActivity(i);
	}

}

Как видно из кода, мы загружаем файл разметки при показе Actvity и в зависимости от того, что находится в переменной GAME_MODE — файлы разметки разные.

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

Если у Вас есть вопросы или комментарии пишите на мыло или оставляйте в комментах.

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

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

17 Responses to Пишем змейку для Android (Часть 1)

  1. erbol говорит:

    Респект. Жду продолжения

  2. Asshaker говорит:

    Очень хорошие коменты, но все равно не очень понятно 🙁
    2й раз на джаве пишу. Переписал весь код в eclipse и он показывает кучу предупреждений и не хочет запускаться 🙁
    P/S/

  3. AVL говорит:

    А если в методе nextmove не повторять код проверки 4 раза, а в операторе switch просто задать значения 2х коэффициентов, например dx и dy, которые могут быть (1 0), (0 1), (-1 0), (0 -1). И потом исходя из этих коэффициентов рассчитывать и обрабатывать координаты следующей точки? Производительность почти не изменится, а код будет и меньше, и красивее.

  4. fon говорит:

    Jebutsa glaza iz-za fona, nerealno voobshe nahoditsa na sajte! — Monitor 24 «

  5. milssky говорит:

    в case DIR_SOUTH:, наверное, надо работать все ж с Y координатой. На кой нам nextX и mFieldX, когда мы вниз двигаться должны 🙂

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

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

  7. Андрей говорит:

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

  8. sasha говорит:

    а де писать кого клас

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

    в каком месте тут:»управлять ее будем с помощью акселерометра»?

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

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