Вернусь к тематике игр для 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-подлянке. Бывает, что подвешивают кул-хацкеры без ведома владельца сайта. Вы уж не поленитесь проверить, а то как-то печально…
А что именно вызвало Ваши подозрения? )))
Не могу всё равно создать помогите