Всем привет! Сегодня мы начнем писать змейку для 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, когда мы вниз двигаться должны 🙂
кардинально отличается от других уроков своими замечательными пояснительными вставками, еще бы вопросы автору задать.
А то задумал баалшои проект, вопрос о изучении языка не стоит, знаю что смогу, но вот построение плана проекта это проблема, и еще б таким же образом пояснили программирование цветов, и обработку изображений, например как убирается белый фон путем кода, ведь не фотошопом же.
Хм, как это не печально — но в основном фотошопом 🙂
Ах да, еще бы было неплохо увидеть урок о том как создать игровое окно самоподстраивающееся под разные форматы экранов.
а де писать кого клас
Эм, вы это про что???
в каком месте тут:»управлять ее будем с помощью акселерометра»?
Наверно имелось в виду «ею»