Всем привет! Сегодня мы начнем писать змейку для 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 — файлы разметки разные.
Ну пожалуй хватит на сегодня. В следующий раз будем разбираться с отрисовкой.
Если у Вас есть вопросы или комментарии пишите на мыло или оставляйте в комментах.
Респект. Жду продолжения
Счас вот пытаюсь как можно более наглядно изложить материал 🙂
Очень хорошие коменты, но все равно не очень понятно 🙁
2й раз на джаве пишу. Переписал весь код в eclipse и он показывает кучу предупреждений и не хочет запускаться 🙁
P/S/
Во второй части статьи внизу есть готовый проект для eclipse
А если в методе nextmove не повторять код проверки 4 раза, а в операторе switch просто задать значения 2х коэффициентов, например dx и dy, которые могут быть (1 0), (0 1), (-1 0), (0 -1). И потом исходя из этих коэффициентов рассчитывать и обрабатывать координаты следующей точки? Производительность почти не изменится, а код будет и меньше, и красивее.
Думаю идея хороша!
Jebutsa glaza iz-za fona, nerealno voobshe nahoditsa na sajte! — Monitor 24 «
Так лучше? 🙂
Согласен
в case DIR_SOUTH:, наверное, надо работать все ж с Y координатой. На кой нам nextX и mFieldX, когда мы вниз двигаться должны 🙂
кардинально отличается от других уроков своими замечательными пояснительными вставками, еще бы вопросы автору задать.
А то задумал баалшои проект, вопрос о изучении языка не стоит, знаю что смогу, но вот построение плана проекта это проблема, и еще б таким же образом пояснили программирование цветов, и обработку изображений, например как убирается белый фон путем кода, ведь не фотошопом же.
Хм, как это не печально — но в основном фотошопом 🙂
Ах да, еще бы было неплохо увидеть урок о том как создать игровое окно самоподстраивающееся под разные форматы экранов.
а де писать кого клас
Эм, вы это про что???
в каком месте тут:»управлять ее будем с помощью акселерометра»?
Наверно имелось в виду «ею»