Ще одна Змійка не в 30 рядків на Android

Привіт всім! Сьогодні я хочу розповісти історію створення однієї іграшки для Android. Що буде:
  • Навіщо ще одна змійка для андроїда?! Пояснення (з описом);
  • Як я це зробив — використані прийоми, трохи коду;
  • Трохи про дизайн;
  • Google Play Games, нестандарное використання.


Всіх зацікавлених прошу під кат!


Навіщо ще одна змійка для андроїда?!
Так сталося, що восени ми з моєю дівчиною відпочивали на морі. Тепла вода, штиль, плавання — все це сприяє народженню різних думок в голові. Однією з таких думок було «Що робити в літаку на зворотному шляху?». Відпочинок був у самому розпалі, і я трохи нудьгував по програмуванню. Отже, вирішено — кодити! На чому? Так на 7" Nexus ж нещодавно AIDE встановив. Що? Гру, просту. Але — з родзинкою. Так народилася ідея написати Змійку з шестикутними тайлами — не банально, цікаво.

Відразу постало питання вибору технологій. Минулої свою гру (Pacman) я писав на С++ + OpenGL ES 2.0, тут же це явний оверхед. Щось краєм вуха чув про Canvas, трохи почитав і зрозумів — те що потрібно, дайте два. 4.5 години в літаку — готовий движок, змійка ходить, врізається сама в себе, і реагує на свайпи в шести напрямках. А потім… потім був ще приблизно місяць допиливаний.

Що вийшло в результаті:

На скріншотах: Головний екран, Лабіринт «Млин»(темний скін), Лабіринт «Телепорт» (світлий скін)

  • 2 види ігри — лабіринти і класична;
  • 3 скіна;
  • 7 рівнів складності, що відкриваються один за іншим.


Бонусом пішов примітивний редактор карт з можливістю відразу ж запустити гру і спробувати, а так само купа нового досвіду.

Як я це зробив
Почнемо по порядку.

Головний екран

Він же екран «паузи», з іншого розкладкою компонентів.
На цьому екрані звернемо увагу на тло. Я витратив на нього цілий вечір, і все одно мені іноді здається, що він виглядає трохи не так, як я хочу. Ідея зробити його саме таким була частково взята у гугла, частково у 2Gis — подобаються мені ці пересічні широкі промені. Фон векторний, тобто він займає в апк лічені байти. Малюється він на Canvas, в кастомном в'ю.
Трохи коду
public class BackgroundView extends View {

/* конструктори опущені*/

private Bitmap mBmp;

@Override
protected void onDraw(Canvas canvas) {
/* Кешируем фон, тому що малювати його при кожній перемальовуванні занадто накладно*/
if(mBmp == null){
mBmp = Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(), Config.ARGB_8888);
Canvas c = new Canvas(mBmp);
drawBackground©;
}

canvas.drawBitmap(mBmp, 0, 0, null);
}

private void drawBackground(Canvas canvas){
canvas.drawRGB(180, 220, 70); /* У цьому класі дуже багато магічних констант, подбиравшихся на ходу*/

/* Тут за допомогою Path малюємо кілька широких променів, різними відтінками зеленого, з різною прозорістю*/

}
}


Що ми виносимо з цього коду? Спосіб перший (і самий простий ) малювання на Canvas. Наследуемся від
View
, перевизначаємо метод
void onDraw(Canvas canvas)
і малюємо на Canvas все, що душі завгодно. Саме цим способом отрисовывался мій перший прототип, написаний в літаку. Для динамічного оновлення треба викликати метод
invalidate()
View
, після чого коли-небудь в майбутньому викличеться onDraw(). Без гарантій. Більш правильний спосіб отримання Canvas для інтенсивного малювання описаний нижче.

Екран налаштувань


Тут, в принципі, нічого особливого немає, за винятком превью гри. Здається, це досить спірне рішення, але мені хотілося, щоб при зміні скіна гравець міг відразу побачити, що його чекає. Надалі кількість скінів планую збільшити. Так само, на пристроях, що підтримують вібрацію, на цьому екрані можна включити/вимкнути її.

Екран вибору Лабіринту


Поки в грі не надто багато лабіринтів — 8. Надалі планую додавати їх. У кожному рядку є іконка лабіринту, його назва, особистий і світовий рекорди. У випадку, коли гравець не залягання в Google Play Games, його особистий рекорд автоматично стає світовим. Лабіринти відкриваються послідовно, як і рівні складності. І ось тут, мабуть, настав час розповісти про нестандартний спосіб використання Google Play Games…

Нестандартне використання Google Play Games
А точніше, підсистеми досягнень. Я назвав це share-over-achievements. Напевно не я перший це придумав, але описів такого способу я не знаходив. Не бийте ногами=)
Коли в грі з'являються якісь «розблокування» — наприклад, відкриття рівня або лабіринту, як у моєму випадку, відразу постає питання — а що, якщо у гравця два девайса? Смартфон і планшет, наприклад? Як дати йому використовувати контент, розблокований на одному девайсі, скрізь? Якщо у вас є сервер, і час на його підтримку, то все в ажурі, можна нишпорити через нього. А якщо, як у мене, ні того, ні іншого, то може підійти наступний трюк:
В момент, коли користувач розблокував деякий контент, розблокуйте ачивмент.
Кэповский код
Games.Achievements.unlock(getApiClient(), achievementName);

А коли потрібно перевірити контент на відкритість, перевірте заодно цю ачивку (можна заздалегідь).
код
PendingResult<LoadAchievementsResult> pendingResult = Games.Achievements.load(getApiClient(), true);
pendingResult.setResultCallback(new LoadAchievementsResultCallback());

При цьому завантажені досягнення краще кешувати десь, звідки ви швидко зможете дістати їх. А так само кешувати розблоковані досягнення, у випадку, якщо гравець не залягання — потрібно буде розблокувати їх в той момент, коли він все-таки залягання.

Чому це нестандартний спосіб використання Google Play Games? Тому що документація в основному показує нам, що досягнення — це така свистілка для підняття настрою користувача. Але вони можуть бути ще й реально корисні. І я вважаю, що це круто. Даний спосіб я використовую для розблокування лабіринтів і рівнів складності.

Тепер поговоримо про саму гру

Поле гри квадратне, 15х15 гексагонов. Логічно представлено двовимірним масивом тайлів, кожен з яких має:
  • Координати наступного за ним» тайла — використовується в теле змійки і телепортах;
  • Прапор «смертельності» — знову ж таки, тіло змійки і стіни лабіринтів;
  • Тип тайла — їжа, супер їжа, змія, стіна, телепорт, порожньо;
Таким чином, перевірка на смерть дорівнює перевірка одного прапора, переміщення через телепорт не затратніше просто переміщення. Отрісовиваємих полі за допомогою класів, що реалізують інтерфейс
IDrawer
, тому кількість скінів може збільшуватися з часом.
я описував найпростіший спосіб відтворення на Canvas. Більш правильний спосіб для динамічної відтворення полягає в успадкуванні від
SurfaceView
і відображенні в окремому потоці. Зупинюся на головних моментах:
Наследуемся від
SurfaceView
, реалізуємо
SurfaceHolder.Callback
для отримання подій
SurfaceView
:
Код
public class GameView extends SurfaceView implements SurfaceHolder.Callback{

public GameView(Context context){
super(context);
getHolder().addCallback(this);
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override 
public void surfaceCreated(SurfaceHolder holder){
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
}


Пишемо потік відтворення
DrawThread
:
Код
public class DrawThread extends Thread {

private boolean mRunning = false;
private SurfaceHolder mSurfaceHolder;
public DrawThread(SurfaceHolder surfaceHolder) {
mSurfaceHolder = surfaceHolder;
}
public void setRunning(boolean running) {
mRunning = running;
}

@Override
public void run() {
Canvas canvas;
while (mRunning) {
canvas = null;
try {
canvas = mSurfaceHolder.lockCanvas(null);
if (canvas == null){
continue;
}
/* Малюємо на canvas */
} finally {
if (canvas != null) {
mSurfaceHolder.unlockCanvasAndPost(canvas);
}
}
try{
//let other threads do they work
Thread.sleep(15);
}catch(InterruptedException e){
}
}
}
}


Повертаємося в наш
SurfaceView
і стартуємо потік візуалізацію при створенні Surface:
Код
@Override
public void surfaceCreated(SurfaceHolder holder) {
/* Тут можна провести ініціалізацію механізмів відтворення, Canvas отримувати так само, як у потоці відтворення */
mDrawThread = new DrawThread(holder);
mDrawThread.setRunning(true);
mDrawThread.start();
}


І не забуваємо прибрати за собою при руйнуванні Surface:
Код
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
destroy();
}

/* Може виникнути проблема, що surfaceDestroyed() не викликається відразу, тоді можна смикнути destroy() "руками" */
public synchronized void destroy(){
if(mDrawThread == null){
return;
}
boolean retry = true;
mDrawThread.setRunning(false);
while (retry){
try{
mDrawThread.join();
retry = false;
}
catch (InterruptedException ignored){
}
}
mDrawThread = null;
}



У бойовому коді
IDrawer
та
Ігри
я передаю в
GameView
. У потоці відтворення відбувається так само і перерахунок логіки.

Висновки
Написання невеликих ігор (нехай навіть і клонів), а так само власних неігрових проектів надзвичайно корисно. Особливо корисний вихід із зони комфорту, він дозволяє в короткі терміни дізнатися багато нового і корисного. З маленького проектика на кілька годин можна зробити цілком цікаву гру.
Технологія малювання на Canvas цілком підходить для невеликих ігор, де не потрібні особливі спецефекти.
Писати ігри на Java під Android набагато простіше, ніж на З++, особливо якщо ти одинак.
Всім гарних вихідних, і з наступаючим Новим роком!

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.