Вернусь к тематике игр для 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

Проблема в том что не всегда случайно расставленную игру можно выиграть. Неплохо было бы сделать проверку на возможность выигрыша.
Хм, вообще-то хорошая идея, тока надо поискать алгоритм 🙂
Когда-то я пробовал русскую 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 часа пятнашки гонял дособрать не мог, аж стыдно перед друзьями было. Думаю, неужто я туп настолько. А оно эвоно как…
Спасибо за информацию. ))))
А вкусняшку вроде .apk как в крестиках можите скинуть??
У вс замечательное пособие, просто и понятно, огромное вам спасибо=))
Думаю можно 🙂 Сделаю более-менее адекватную игру из этого и выложу тем более что там почти вся игра уже есть 🙂
Пожалуйста, ловите проект для Eclipse 🙂
Привет, скиньте пожалуйста проект для Eclipce)
так ведь есть!
В конце статьи посмотрите!
Сразу три вопроса. Как открыть в eclipse чужие проекты, я кидаю их в workspace но среда их не видит. Как подключить ваш движок к своему проекту?(извините за вопросы, но мы же новички). Третий, где на вашем сайте реклама, что бы потискать её, нам не сложно — а Вам будет приятно!!!
Ответы на первый и второй вопрос выложил здесь.
По третьему вопросу: ну нет рекламы у меня в блоге 🙂 Хоть весь обыщите не найдете 🙂 Разве это плохо когда сайт без рекламы?
Хотите сделать мне приятно? Скачайте игрушку и поставьте ей побольше звезд (в ней тоже нет рекламы) вам не сложно, а мне будет приятно 🙂 или запостите ссылку у себя в социалке — вам вообще один клик а мне приятно что сюда заходят новые люди и может быть узнают что-то полезное 🙂
Есть небольшой вопрос. По поводу этого урока. Как сделать чтобы была рандомность спрайтов? Например чтобы каждый раз цифры были разного цвета. Например одного цвета хранятся в папке «w», другие в папке «t» и т.д.
Сори если не понятно, что хочу спросить. Я просто совсем новичок во всём этом(
Думаю можно просто использовать разные спрайты. Или перезагружать спрайт.
Так я и использую разные спрайты. Спрайты одного цвета хранятся в папке «w», а другого в папке «t». Я хочу сделать так чтобы при создании Activity самой игры радномно выбиралось какого цвета будут спрайты, т.е. из какой папки их брать. Как бы правильно это прописать в коде?
Ну попробуйте например разные спрайты одного и того же объекта запихать в массив. И случайным образом выбирать элемент из этого массива.
Спасибо, попробую)
Я думаю проблем у Вас с этим возникнуть не должно 🙂
Прочитал все статьи про движок. Спасибо! Вот такой вопрос возник — можно ли используя Ваш движок заставить спрайт(анимированный) двигаться по какой-либо траектории? Во многих играх используется прием псевдопроизвольного движения персонажа(допустим герой которым должна управлять сама игра).
Да, можно, (линейно правда), и с ускорением тоже можно.
Это надо спрайт «докручивать» или выносить в управляющий поток?
Да нет, ничего не надо. У него есть методы setdx() и setdy() если я не ошибаюсь. Точно уже не помню, сорри 🙁
Огромное спасибо! отличные статьи. Для начинающих то что надо. Все хорошо и доходчиво написано.
Один вопрос: после импорта Вашего проекта эклипс ругается в классе SurfaceView на @Override перед surfaceChanged, surfaceCreated, surfaceDestroyed. Если @Override убрать то все нормально запускается. Только начинаю осваивать андроид, что я не так делаю?
Хм… Не знаю, но если у Вас все работает то и замечательно. Возможно дело в том, что у меня целая куча разных версий этого движка на разных стадиях готовности. Они очень незначительно друг от друга отличаются, и может дело именно в каких-то отличиях того что я использовал при написании и того что я выложил на сайт.
А не кто не продумал правильную генерацию поля?:) Нет, я не говорю что тут есть ошибки, тут есть недочеты. Пятнашки имеют !15 это порядка большое число где половина из этих этих изначальных комбинация, эта половина не имеет решения. Я сталкивался с тем что моя игра на GameMaker постоянно выдавала генерацию поля которая бы не давала решения:)
Ну здесь такого случиться не может. Собственно лень заморачиваться с проверкой, хотя наверняка существует достаточно простой алгоритм проверки.
Ну да, а вместо этого «я решил сделать несколько проще.» 😀
Не проще ли было заглянуть в ту же Wiki за теорией, и за 5 минут решить вопрос?… там кода на 15 строчек при размашистом стиле.
Ну уж простите, не нашел я по-быстрому 🙂
Если найдете присылайте, выложу.
Спасибо за столь замечательные уроки, мне они очень помогли в знакомстве с разработкой под 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 мс и перерасчетами перед отрисовкой, то все будет хорошо, сцена будет меняться как и запланировано. Но если этого времени не будет хватать на отрисовку, что будет происходить? полный цикл отрисовки анимированных спрайтов замедлится? Задачи будут копиться в стеке выполнения одна за другой, пока все они в конце концов не выполнятся? Если да, то что нужно предпринять чтобы этого избежать?
Заранее благодарю за советы и подсказки 🙂
Дело в том, что отрисовка происходит вне зависимости ни от чего — в отдельном потоке. Поэтому логично различные рассчеты выносить в отдельный поток. Таким образом если вы хотите пересчитать новое положение спрайтов например и по каким-то причинам не успели, то спрайт так или иначе отрисуется на своем старом месте. Такой подход имеет как свои плюсы так и свои минусы. Но мне он кажется наиболее оправданным для игр не сильно зависящих от рассчетов.
а как сделать самое приложение?(.apk) просто скомпилировать? Если да, то как игру сделать совместимой с Android 4.0.4?
Правой кнопкой мыши на проекте в eclipse затем выбираете Android Tools и там есть Export (Signed|Unsgned) Application Package
Вы очень хорошо все описываете-спасибо. Мало очень русских ресурсов с внятным языком описания. Все становиться ясным. А уж про движок-как Вы описывали-это просто классика. Я столько всего содрал, мда у Вас. Вот так.
Скачал проект, Апк не видать, да и сам проект в эклипсе не работает.
А какая ошибка? У вас установлена необходимая версия SDK?
Eclipse, sdk все есть последней версии,кидаю проект в домашнюю папку(где проекты), хочу скомпилировать, компилит как Ant build. Скорее, может я че не правильно делаю
Я имею в виду версии установленных платформ. Попробуйте импортировать проект.
вопрос, как импортировать?? да и вопрос еще один, с помощью чего(какого элемента) можно разметить игровое поле, в котором будут двигаться кнопки?
Eclipse не хочет компилировать. В коде находит ошибки, например, в строках x = (int)(math.random()*4);
y = (int)(math.random()*4);
пишет: «math cannot be resolved».
В чем причина?
Попробуйте math с большой буквы 🙂
Там с большой, но не работает.
А можно выложить полный проект с движком для eclipse ? в конце статьи только движок.
Там вроде полный проект 🙂 Он просто называется тка же как и движок
Спасибо! разобрался…
Здравствуйте. Eclipse не запускает проект. Ошибочка такая error: Error: String types not allowed (at ‘layout_height’ with value ‘match_parent’), причем во всех строчках где указано ‘match_parent’. Путь mEngine/res/layout/b.xml. Заранее спасибо.
Странно, а скрин можно?
Да, конечно.
http://i47.fastpic.ru/big/2013/0610/9e/d5cab9a0d16e002f0871620976b88a9e.jpg
А что написано во вкладке Problems?
О, кстати, я кажется понял в чем проблема! Скачайте GalaxyTab 2.2 Addone в SDK manager!
Загрузил весь SDK какой только был, но Galaxy Tab там почему-то не было. Как ни странно, проблему решило банальное удаление этого xml.
Подскажите пожалуйста, как лечится ошибка при импорте файла. Эклипс ругается на строчку
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
Большушее спасибо за исходники, надеюсь, разобраться как и что работает.
Спасибо за блог!
Вопрос такой,вот в src два файла ,вот в чем вопрос,почему 2 ,какой из них главный,в общем с чего начинается ,вот такие вопросы по взаимодействию классов так сказать.Вот если не ошибаюсь в java есть класс main или процедура,который и выполняется,а здесь как MainScreen как я понял но почему?
Здесь вы указываете в файле AndroidManifest какие активити являются главными, какие могут быть вызваны системой и т.д. Тут не совсем как в Java
привет, статья супер) очень хочу своему МЧ сделать сюрприз на 14 февраля, он программист. Вопрос: можно ли вместо цифр вставить его фотографии и чтобы он их собирал.. как это лучше сделать?%)
Чего-то скрипты подозрительные на странице подвешены — очень боты подозревают их в XSS-подлянке. Бывает, что подвешивают кул-хацкеры без ведома владельца сайта. Вы уж не поленитесь проверить, а то как-то печально…
А что именно вызвало Ваши подозрения? )))
Не могу всё равно создать помогите