Программирование змейки для Android (Часть 2, заключительная)

Снова привет!

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

Продолжим! Следующий класс у нас будет наследоваться от SurfaceView — в этом классе мы будем производить прорисовку. Ничего сложного в нем нет. Вообще ничего! Просто конструктор, в котором мы создаем новое игровое поле и загружаем ресурсы (читай картинки) а так же простой метод для отрисовки. Собственно вот его код:

package ru.davidmd.simpleSnake;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.SurfaceView;

public class GameSurface extends SurfaceView {

	SnakeGame mField;

	Bitmap mHead, mTill, mBody, mBg, mFruite;

	String someText = "123";

	float x, y;

	// Установка новых кординат телефона в пространстве
	// для того чтобы правильно нарисовать кружки на фоне
	public void setXY(float x, float y) {
		this.x = x;
		this.y = y;
	}

	// Собственно конструктор в котором мы загружаем
	// из ресурсов битмапы и добавляем метод обратного вызова
	// для нашей Surface
	public GameSurface(Context context) {
		super(context);
		//this.getHolder().addCallback(this);
		mField = new SnakeGame();
		mHead = BitmapFactory.decodeResource(context.getResources(),
				R.drawable.head);
		mTill = BitmapFactory.decodeResource(context.getResources(),
				R.drawable.till);
		mBody = BitmapFactory.decodeResource(context.getResources(),
				R.drawable.body);
		mBg = BitmapFactory.decodeResource(context.getResources(),
				R.drawable.bg);
		mFruite = BitmapFactory.decodeResource(context.getResources(),
				R.drawable.fruite);
	}

	// метод в котором устанавливаем текст
	public void setSomeText(String someText) {
		this.someText = someText;
	}

	// Рисуем здесь
	void drawSnake(Canvas c) {
		int width = c.getWidth();
		int height = c.getHeight();
		int mx = width / SnakeGame.mFieldX;
		int my = height / SnakeGame.mFieldY;
		// стрейчим битмапы
		Bitmap head = Bitmap.createScaledBitmap(mHead, mx, my, true);
		Bitmap body = Bitmap.createScaledBitmap(mBody, mx, my, true);
		Bitmap till = Bitmap.createScaledBitmap(mTill, mx, my, true);
		Bitmap bg = Bitmap.createScaledBitmap(mBg, mx, my, true);
		// создаем на всякий кисточку
		Paint paint = new Paint();
		paint.setColor(Color.CYAN);
		// рисуем кружки
		c.drawCircle(width / 2, height / 2, width / 4, paint);
		paint.setColor(Color.BLUE);
		c.drawCircle(width / 2 - x * 5, height / 2 + y * 5, width / 10, paint);
		paint.setColor(Color.BLACK);
		paint.setAlpha(128);

		Bitmap fruite = Bitmap.createScaledBitmap(mFruite, mx, my, true);
		// рисуем игровое поле с фруктами на нем
		for (int i = 0; i < SnakeGame.mFieldX; i++) {
			for (int j = 0; j < SnakeGame.mFieldY; j++) {
				c.drawBitmap(bg, mx * i, my * j, paint);
				if (mField.getmField()[i][j] > 1) {
					c.drawBitmap(fruite, mx * i, my * j, paint);
				}
			}
		}
		paint.setAlpha(0);
		// рисуем змею
		for (int i = 0; i < mField.getSnakeLength(); i++) {
			c.drawBitmap(body, mField.getmSnake().get(i).x * mx, mField
					.getmSnake().get(i).y * my, new Paint());
			if (i == 0) {
				c.drawBitmap(till, mField.getmSnake().get(i).x * mx, mField
						.getmSnake().get(i).y * my, new Paint());
			}
			if (i == mField.getSnakeLength() - 1) {
				c.drawBitmap(head, mField.getmSnake().get(i).x * mx, mField
						.getmSnake().get(i).y * my, new Paint());
			}
		}
		// рисуем текст
		paint.setColor(Color.WHITE);
		paint.setAlpha(255);
		paint.setTextSize(15);
		c.drawText(someText, 50, 50,  paint);
	}
}

Идем дальше?

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

package ru.davidmd.simpleSnake;

import java.util.List;
import java.util.Timer;

import android.app.Activity;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.view.WindowManager;

public class GameActivity extends Activity implements SensorEventListener {

	GameSurface surf;
	Timer t;
	int width, height;

	SensorManager mSensorManager;
	Sensor mAccelerometerSensor;

	float SSX = 0, SSY = 0;
	float SX = 0, SY = 0;
	boolean firstTime;

	// Ну тут обрабатываем создание активити
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		surf = new GameSurface(this);
		this.setContentView(surf);
		t = new Timer();
		height = this.getWindowManager().getDefaultDisplay().getHeight();
		width = this.getWindowManager().getDefaultDisplay().getWidth();

		// Инициализируем акселерометр
		mSensorManager = (SensorManager) getSystemService(Activity.SENSOR_SERVICE);
		List<Sensor> sensors = mSensorManager.getSensorList(Sensor.TYPE_ALL);
		if (sensors.size() > 0) {
			for (Sensor sensor : sensors) {
				if (sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
					if (mAccelerometerSensor == null)
						mAccelerometerSensor = sensor;
				}
			}
		}
	}

	// Запуск активити
	@Override
	public void onStart() {
		super.onStart();
		// Запускаем таймер обновления картинки на экране
		t.scheduleAtFixedRate(new GraphUpdater(surf), 0, 100);
		// Запускаем таймер обновления положения змейки
		t.scheduleAtFixedRate(new StepUpdater(this), 0, 500);
		// регистрируем нашу форму как объект слушающий
		// изменения датчика - акселерометра
		mSensorManager.registerListener(this, mAccelerometerSensor,
				SensorManager.SENSOR_DELAY_GAME);
		this.firstTime = true;
		getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
	}

	// Обрабатываем остановку активити
	@Override
	public void onStop() {
		super.onStop();
		// Останавливаем таймеры
		t.cancel();
		t.purge();
		// Отписываемся от получения сообщений об изменении
		// от датчика
		mSensorManager.unregisterListener(this);
	}

//	@Override
	public void onAccuracyChanged(Sensor sensor, int accuracy) {
		// Do nothing!
	}

	// метод, который определяет по показаниям акселерометра
	// передаваемым ему как параметры (х и у)
	// в каком направлении должна двигаться змея
	private int getDirection(float x, float y) {
		if (Math.abs(x) > Math.abs(y)) {
			if (x > 0) {
				return SnakeGame.DIR_WEST;
			} else {
				return SnakeGame.DIR_EAST;
			}
		} else {
			if (y > 0) {
				return SnakeGame.DIR_SOUTH;
			} else {
				return SnakeGame.DIR_NORTH;
			}
		}
	}

	// А вот так мы обрабатываем изменение ориентации
	// телефона в пространстве
//	@Override
	public void onSensorChanged(SensorEvent event) {
		surf.setSomeText("Your score is: "+SimpleSnakeActivity.GAME_SCORE);

		// получаем показания датчика
		SX = event.values[0];
		SY = event.values[1];

		// Если игра уже идет, то
		if (!this.firstTime) {
			// получаем положение телефона в пространстве
			// с поправкой на его начальное положение
			float dirX = SX - SSX;
			float dirY = SY - SSY;
			// Устанавливаем для змеи новое направление
			surf.mField.setDirection(this.getDirection(dirX, dirY));
			// передаем в нашу повержность координаты телефона в пространстве
			surf.setXY(dirX, dirY);
		} else {
			// Если игра только началась делаем поправку на начальное
			// положение телефона
			this.firstTime = false;
			SSX = SX;
			SSY = SY;
		}
	}

	// Этот метод вызывается из потока одного из таймеров
	// именно в этом методе происходит движение змейки в
	// зависимости от ее направления установленного в предидущем
	// методе
	public void Step() {
		// Если ход не удался то закрываем текущую активити
		if (!surf.mField.nextMove()) {
			SimpleSnakeActivity.GAME_MODE=1;
			this.finish();
		}
		// Если все впорядке то обновляем очки
		// в стартовой активити
		else{
			SimpleSnakeActivity.GAME_SCORE=this.surf.mField.mScore;
		}
	}
}

Теперь давайте смотреть, что же тут к чему! В переменных имеется surf — экземпляр GameSurface, таймер t, SensorManager и Sensor. Все это и еще немного другого пригодится нам для того чтобы наша змея начала жить своей жизнью 🙂 (под нашим чутким руководством). Посмотрим что у нас имеется в методе onCreate() : первым делом мы создаем игровую поверхность (surf) и размещаем ее на нашей активити. Затем инициализируем таймер. Следующим шагом получаем ширину и высоту экрана.  А вот дальше самое на мой взгляд интересное. С помощью метода getSystemService() мы получаем значение для переменной mSensorManager, а у нее уже запрашиваем список всех датчиков, установленных на устройстве. Затем в цикле перебираем все датчики до тех пор, пока не найдем акселерометр! Ура, сделано!

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

Ну, думаю в методе onStop() ничего сложного нет, там думаю и так все понятно. В методе getDirection() мы определяем в какую сторону должна двигаться змея в зависимости от отклонения телефона по оси x или y. Сами отклонения передаются сюда как параметры. ну и следующий интересный метод — onSensorChanged(). Он вызывается тогда, когда меняется положение телефона в пространстве. Прежде всего мы получаем сырые данные от датчика, затем проверяем, если игра только что открылась, и это первый раз, когда мы считываем координаты, то принимаем текущее положение телефона за эталонное. Это нужно для того, чтобы пользователь мог играть не только расположив телефон горизонтально. В таком случае текущие координаты становятся поправкой. Если мы уже не первый раз считываем положение датчика, тогда высчитываем отклонение смарта в ту или иную сторону исходя из начальной поправки и текущих показаний датчика. И устанавливаем новое направление движения змеи.

Последний метод в этом классе — step(). В его задачи входит подать команду змее на новый ход и посмотреть, что ответит змея. Если змея может туда идти, то все нормально, а если нет, то тут мы закрываем нашу активити и автоматом попадаем на предыдущую, о которой я писал в прошлый раз.

Ну и самое последнее — собственно то что и оживляет всю-всю нашу игру! Это два очень схожих класса: GraphUpdater и StepUpdater. Оба класса являются наследниками TimerTask. Т.е. — это задания таймера, которые мы можем запланировать. (Что мы и сделали в методе onStart()). Каждый из них занят своим очень важным делом. GraphUpdater при создании должен получить экземпляр GameSurface. Само задание заключается всего лишь в вызове метода отрисовки! Вот код этого класса:

package ru.davidmd.simpleSnake;

import java.util.TimerTask;

import android.graphics.Canvas;
import android.graphics.Color;

public class GraphUpdater extends TimerTask {

	GameSurface surf;

	GraphUpdater(GameSurface surf){
		this.surf = surf;
	}

	@Override
	public void run() {
		Canvas c = surf.getHolder().lockCanvas();
		if (c!=null){
			c.drawColor(Color.BLACK);
			surf.drawSnake(c);
			surf.getHolder().unlockCanvasAndPost(c);
		}
	}
}

А что же делает StepUpdater? Ну тут все еще проще! Он просто с вызывает метод step() из переданной ему в конструктор GameActivity. Сам код:

package ru.davidmd.simpleSnake;

import java.util.TimerTask;

public class StepUpdater extends TimerTask {

	GameActivity act;

	StepUpdater(GameActivity s){
		this.act = s;
	}

	@Override
	public void run() {
		act.Step();
	}

}

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

На этом у меня все! Игра готова!

UPD вот готовый проект по просьбам трудящихся.

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

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

47 комментариев на «Программирование змейки для Android (Часть 2, заключительная)»

  1. FORWARD говорит:

    Напишите про то, как вставлять баннеры в свои приложения и получать проценты от рекламодателя

  2. FORWARD говорит:

    И да, спасибо за очень полезные(а главное понятные) статьи

  3. erbol говорит:

    А на github нельзя проект выложить с рисунками и оформлением? Было бы хорошо.

    Спасибо

    • davidmd говорит:

      Да как-то времени нет. Проект сам могу потом тут выложить 🙂 чтоб не заморачиваться.

      • Алексей говорит:

        Пожалуйста выложите либо весь проект, либо картинки к нему, я не знаю и не смог найти статьи в каком формате надо картинки.
        Вот код ваш:
        mField = new SnakeGame();
        mHead = BitmapFactory.decodeResource(context.getResources(),
        R.drawable.head);
        mTill = BitmapFactory.decodeResource(context.getResources(),
        R.drawable.till);
        mBody = BitmapFactory.decodeResource(context.getResources(),
        R.drawable.body);
        mBg = BitmapFactory.decodeResource(context.getResources(),
        R.drawable.bg);
        mFruite = BitmapFactory.decodeResource(context.getResources(),
        R.drawable.fruite);
        У меня тут постоянно ошибка из-за того что нет картинок, а те которые я сам сделал в формате bmp не подходят.
        Выложите либо сам проект пожалуйста либо картинки что-ли.. можно на мейл.

  4. Riki говорит:

    Подскажите — запустил игру на машине, но при нажатии на кнопки змейка не двигается…корректно работает только на устройстве?

  5. Роман говорит:

    У меня ваш проект при импорте в eclipse выдает ошибку в файле StepUpdater.
    Плюс не один .xml не открывается. В консоле ошибка: Unable to resolve target ‘android-7’
    Может сталкивались с этой проблемой кто. Как это убрать?

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

      Выдавало аналогичную ошибку помог следующий комментарий на форуме: «right-click on your project and select «Properties -> Java Compiler», check «Enable project specific settings» and select 1.5 or 1.6 from «Compiler compliance settings» select box.»
      После выбора версии 1.6 всё удачно запустилось.

  6. lev говорит:

    Доброе время суток.
    Хотел добавить паузу, можете подсказать как это можно сделать?
    Заранее спасибо =)

  7. Станислав говорит:

    Ссылка на полный проект активна или нет? У меня ничего не происходит при нажатии

    • davidmd говорит:

      Оппа! Ссылка почему-то не работает постараюсь в ближайшие пару дней найти и загрузить приложение заново

  8. Developer говорит:

    Ссылку ссылку… дайте ссылку на готовый проект!!!
    Спасибо.

    • davidmd говорит:

      Да я в общем-то и не помню где у меня этот проект 🙁 Разве что могу попробовать заново собрать его 🙂

  9. Developer говорит:

    Если не трудно, буду очень признателен!
    И если можно пришлите на kovtyn.dima@gmail.com
    Еще раз благодарю!

  10. Developer говорит:

    Уважаемый davidmd, так что на счет ссылки или готового проекта, а что то у меня по мануалу ну никак собрать не получается…
    Буду очень-очень признателен.

  11. Кирилл говорит:

    Так я не понимаю, почему нет препятсвий в готовом проекте?

  12. kot говорит:

    Подскажите пожалуйста, как сделать чтобы скорость менялась от угла наклона телефона (как мне высчитать этот угол акселератора)?
    Если не затруднить, помогите с данным вопросом)

  13. Sashasl00 говорит:

    А не подскажете, как сделать так, чтобы счет не сбрасывался при перезагрузке игры?

  14. Dim говорит:

    А если делать управление змейки с помощью нажатия на экран? сильно ли придется изменять код?

    • davidmd говорит:

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

  15. WinWon говорит:

    В этой части кода Eclipse подчеркивает getHolder(). / drawSnake(c), / getHolder() снова. В чем может быть дело из подсказок эклипса не понимаю 🙁 советует три варианта: 1. изменить на getHandler() 2. создать метод getHolder() в GameSurface() 3. Добавить cast в ‘surf’. =(

    @Override
    public void run() {
    Canvas c = surf.getHolder().lockCanvas();
    if (c!=null){
    c.drawColor(Color.BLACK);
    surf.drawSnake(c);
    surf.getHolder().unlockCanvasAndPost(c);
    }
    }

  16. LEO говорит:

    Программа работоспособная. Нашел только одну ошибочку: перепутана координата «Y» на «X» когда двигаемся на юг (т.е. вниз).
    А так, очень хороший пример работы акселерометра.
    Ну и хорошо было бы добавить паузу в игре. И голову со стрелкой хотя бы! А то не понятно куда она движется))
    Автору респект!

    • davidmd говорит:

      Если вы это исправите и добавите — с удовольствием выложу здесь, безусловно указав Вас ))

      • LEO говорит:

        Скоро доделаю рабочий вариант и дам Вам ссылочку! 🙂

      • LEO говорит:

        Вот и ссылка на готовую первую версию:
        https://yadi.sk/d/yl4T1y98dDVmX

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

        Удачи и спасибо за скелет программы!

  17. Pegi говорит:

    открыаю готовый проект в eclipce , а он пустой
    как открыть?

  18. TWIGGYISHAPPY говорит:

    Добрый день!
    Подскажите, пожалуйста, как исходник можно запустить? Для какой версии предназначен данный исходник?
    В eclipse, idea и android studio не запускается с такой проблемой (на картинке):
    https://pp.vk.me/c625129/v625129090/204c6/6vsxRVuuyG8.jpg

  19. Larion говорит:

    Не подскажете, как убрать само изображение акселерометра в центре экрана?

  20. ilia говорит:

    В Android Studio построил .apk. При запуске на устройстве (Android Marshmallow 6.0.1) моментально вылетает. Что подскажете?

    • davidmd говорит:

      О, наверно вам надо написать с какой ошибкой и на какой строке 🙂 Посмотрите в логе 🙂

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

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