Досвід розробки ігри для Android. Від ідеї до реалізації

Логотип Funglish

У даній статті хочу представити вам досвід розробки гри-тренажера англійської мови для платформи Android, розповісти які труднощі виникли у процесі розробки, а також які помилки були допущені.

Передісторія

З 2011 року я працював Android-розробником в різних компаніях, у тому числі і на фрілансі. За весь цей час брав участь у розробці різних проектів, але так і не знайшов часу написати власну програму. Виходить «чоботар без чобіт» – програміст, у якого є аккаунт на Google Play і досвід участі у різних проектах, але немає власних додатків.

Ідея

У травні 2015 року задумав зробити гру-тренажер для запам'ятовування слів англійською мовою. В Google Play існує величезна безліч таких ігор і додатків, але хотілося зробити що-небудь своє, не схоже на інших. Для цього потрібно було додати якусь «родзинку», яка відрізняла б мою гру від інших. Цим ключовою відмінністю стало обмеження часу, за який користувач повинен встигнути перевести слова. Наявність таймера повинно додати в гру елемент напруги.

Основна концепція гри схожа з іншими іграми – користувач перекладає слова, отримує за це «зірочки», які дозволяють відкривати нові рівні складності. Для подальшої розробки необхідно було провести більш глибоку деталізацію проекту.

Першим ділом вирішив розділити слова за класичним для ігор 3-го рівнів складності: легкий, середній і важкий, де в легкому – найбільш прості і часто вживаються в англійській мові слова, а в складному рівні – рідковживані слова, або рідкісні варіанти перекладу.
Після визначення рівня складності, потрібно було зрозуміти, що буде представляти собою ігровий рівень. Було прийнято рішення, що рівень буде містити 5 слів, переклад яких дається обмежений час. Кожному слову дається 4 варіанти перекладу. Щоб ускладнити завдання, перекладу слова будуть обрані або співзвучні з російським перекладом, або переклади слів, співзвучних з оригінальним англійським словом. Все це зроблено для того, щоб користувач повністю зосередився на програмі і уважно (і в той же час швидко) вибирав відповіді.
Якщо час, відведений на рівень, закінчилося, або користувач помилився з перекладом, рівень повинен починатися заново, при цьому користувачу доступна можливість перегляду правильної відповіді.

При відповіді правильно на всі 5 слів, виставляється рейтинг (від 1-ї до 3-х зірок, в залежності від затраченого часу). При наборі певної кількості слів повинен відкриватися новий рівень складності, в якому буде менше часу на відповіді.

В результаті у нас 3 рівня складності, в кожному з яких 36 карток з 5 слів у кожній. Виходить, при повному проходженні гри користувач вивчить 540 англійських слів різного рівня складності, якщо повністю пройде гру. Досить непогано для початку.

Неймінг

Назва додатка само прийшло на розум – поєднання англійських слів «Fun» і «English», які описують всю суть гри – нескучне вивчення англійських слів.

Дизайн

Для початку на звичайному папері були намальовані скетчі майбутніх екранів програми із зазначенням послідовності переходів між екранами і коментарями для дизайнера. Вийшло всього 10 екранів:
  • заставка
  • головне меню
  • вибір рівня складності
  • вибір ігрового рівня
  • гра
  • вікно «перемога»
  • вікно «поразка»
  • налаштування
  • інформація
  • навчання
Цей матеріал був переданий знайомому дизайнеру, який взявся за роботу. Він розробив логотип, іконку, вибрав основні кольори, шрифти, розміри кнопок та ін. Потім представив готове рішення інтерфейсу, намальоване в векторі, і нарізку. Підготував матеріали для релізу («зображення», рекламні зображення тощо).

Результат роботи можете побачити на скріншотахWhite-screen

Головне меню

Вибір рівня

Гра

Вікно перемоги

Вікно поразки

Вікно навчання


Розробка

Основні моменти
Мінімально підтримуваної версією Android був обраний API 15 (Android 4.0.3).
Програми не потрібні додаткові дозволи (Android Permissions), т. к. в додатку немає ні реклами, ні збору статистики.

У проекті всього 2 Activity: SplashActivity і GameActivity, в останньому екрани змінюються шляхом зміни різних фрагментів, яких всього 7:
  • InfoFragment — інформація про розробника
  • MainFragment — головне меню
  • PlayFragment — гра
  • SelectDifficultFragment — вибір рівня складності
  • SelectLevelFragment — вибір ігрового рівня
  • SettingsFragment — налаштування
  • TutorialFragment — навчання
База даних за своєю структурою дуже проста, тому зроблена традиційним для Android способом, без використання яких-небудь допоміжних бібліотек (greenDao або ін).
Структура бази даних
public static abstract class BaseTable {
public static final String _ID = BaseColumns._ID;
}

public static class LevelResultTable extends BaseTable {
private static final String TABLE_NAME = "LevelResult";

public static final String DIFFICULT = "difficult";
public static final String LEVEL_NUMBER = "level_number";
public static final String STARS = "stars";

private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + _ID + " INTEGER PRIMARY KEY UNIQUE, " +
DIFFICULT + " INTEGER NOT NULL, " +
LEVEL_NUMBER + " INTEGER NOT NULL, " +
STARS + " INTEGER NOT NULL, " +
" UNIQUE (" + DIFFICULT + ", " + LEVEL_NUMBER + ")" +
") ;";
}

public static class WordTable extends BaseTable {
private static final String TABLE_NAME = "Word";

public static final String ORIGINAL = "original";
public static final String DIFFICULT = "difficult";
public static final String LEVEL = "level";
public static final String ANSWER = "answer";
public static final String TRANSLATE1 = "translate1";
public static final String TRANSLATE2 = "translate2";
public static final String TRANSLATE3 = "translate3";
public static final String TRANSLATE4 = "translate4";
public static final String TRANSLATE5 = "translate5";

private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + _ID + " INTEGER PRIMARY KEY UNIQUE, " +
DIFFICULT + " INTEGER NOT NULL, " +
LEVEL + " INTEGER NOT NULL, " +
ORIGINAL + " TEXT UNIQUE NOT NULL, " +
ANSWER + " TEXT NOT NULL, " +
TRANSLATE1 + " TEXT NOT NULL, " +
TRANSLATE2 + " TEXT NOT NULL, " +
TRANSLATE3 + " TEXT NOT NULL, " +
TRANSLATE4 + " TEXT NOT NULL, " +
TRANSLATE5 + " TEXT NOT NULL " +
");";
}

Можна помітити, що в таблиці WordTable присутнє поле answer і поля translate1, translate2, translate3, translate4, translate5, що в сумі дає 6 варіантів відповіді, хоча в програмі на екрані відображається лише 4. Додаткові 2 варіанти відповіді використовуються для того, щоб користувач при повторному проходженні рівня побачив частково інші варіанти відповіді.


При розробці першої версії орієнтувалися лише на телефони, тому в AndroidManifest була дописана рядок
<supports-screens android:xlargeScreens="false"/>
щоб обмежити список підтримуваних пристроїв.

Вміст
Популярність будь-якого додатка (якщо не брати до уваги будь-якої утиліти або вузькоспрямовані професійні програми) безпосередньо залежить від якості його змісту.

Як вже згадував вище, в додатку 3 рівня складності. Слова для кожного рівня підбиралися вручну, причому складність оцінювалася суб'єктивно. Для додаткового ускладнення гри при пошуку слів намагався використовувати співзвучні і схожі за написанням або змістом слова, наприклад, якщо оригінальне слово «Something», то варіантами перекладу будуть:
  • щось
  • Де
  • Кудись
  • Когось
  • Навіщо
  • Чомусь


Блюр
Коли по закінченню рівня з'являється вікно результату («поразка» або «успіх»), задній фон стає «заблюренным». Ефект блюр реалізований класичним способом: зберігаємо Bitmap ігрового Layout'а, обробляємо його і ставимо фоном у вікно результату.

Обробка зображення була реалізована за допомогою коду Mario Klingemann (mario@quasimondo.com), який був знайдений по посилання. Обробка зображення є дуже тривалим процесом, тому для прискорення обробки зображення попередньо зменшується. Як це часто буває, втрачаючи в якості виграємо у часі. У даному випадку втрати якості не критичні.

Код методу для створення скріншоту
public class ScreenShot {
public static Bitmap getScaledScreenshot(View v, float scaleFactor) {
Bitmap b = Bitmap.createBitmap((int) (v.getWidth() / scaleFactor), (int) (v.getHeight() / scaleFactor), Bitmap.Config.RGB_565);
Canvas c = new Canvas(b);
c.scale(1.f / scaleFactor, 1.f / scaleFactor);
v.draw©;
return b;
}
}


Код класу для зменшення зображення
public class Resize {
private static Paint sPaint = new Paint(Paint.FILTER_BITMAP_FLAG);

public static Bitmap scale(Bitmap bmp, float scaleFactor, boolean recycleOriginalBmp) {
Bitmap overlay = Bitmap.createBitmap((int) (bmp.getWidth()/scaleFactor),
(int) (bmp.getHeight()/scaleFactor), Bitmap.Config.RGB_565);

Canvas canvas = new Canvas(overlay);
canvas.scale(1 / scaleFactor, 1 / scaleFactor);
Paint paint = new Paint();
paint.setFlags(Paint.FILTER_BITMAP_FLAG);
canvas.drawBitmap(bmp, 0, 0, paint);

if(recycleOriginalBmp) {
bmp.recycle();
bmp = null;
}

return overlay;
}
}


CustomView
Для підвищення продуктивності і для поліпшення відображення деяких екранів було написано кілька власних View і ViewGroup, наприклад, кнопка відповіді на питання.

Текст кнопок зі словами повинен бути максимально більшими, але в той же час кількість букв у слові може відрізнятися від слова до слова. Тому був зроблений автоматичний підгін розміру тексту, якщо він перевищує деяку ширину (ширина View за вирахуванням відступу).

Код кнопки
public class AnswerButton extends View {
//private static final String TAG = AnswerButton.class.getSimpleName();
public static final int STATE_NORMAL = 0;
public static final int STATE_SUCCESS = 1;
public static final int STATE_FAILED = 2;
public static final int STATE_PRESSED = 3;

private Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);


private static Bitmap sBackgroundBitmap, sPressedBitmap, sSuccessBitmap, sFailedBitmap;

private int mState = STATE_NORMAL;
private int mWidth;
private int mHeight;

private float mTextLeftX, mTextTopY;
private float mLeftRightPadding;

private Rect mBackgroundRect = new Rect(), mTextBounds = new Rect();

private float mTextSize;

private String mText;

public AnswerButton(Context context) {
super(context);
init();
}

public AnswerButton(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

public AnswerButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}

private void init() {
if(sBackgroundBitmap == null) {
sBackgroundBitmap = BitmapFactory.decodeResource(getResources(), R. drawable.btn_answer_normal);
}

if(sSuccessBitmap == null) {
sSuccessBitmap = BitmapFactory.decodeResource(getResources(), R. drawable.btn_answer_success);
}

if(sFailedBitmap == null) {
sFailedBitmap = BitmapFactory.decodeResource(getResources(), R. drawable.btn_answer_failed);
}

if(sPressedBitmap == null) {
sPressedBitmap = BitmapFactory.decodeResource(getResources(), R. drawable.btn_answer_pressed);
}

setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction() & MotionEvent.ACTION_MASK;
if(action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) {
if(mState == STATE_PRESSED) {
return false;
}
mState = STATE_PRESSED;
invalidate();
} else {
if(mState != STATE_PRESSED) {
return false;
}

mState = STATE_NORMAL;
invalidate();
}
return false;
}
});

mLeftRightPadding = getResources().getDimension(R. dimen.view_answer_button_left_right_padding);
mTextSize = getResources().getDimension(R. dimen.answer_button_text_size);

mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(getResources().getColor(R. color.answer_button_text_color));
mTextPaint.setTypeface(FontManager.VDS_COMPENSATED_LIGHT);
}

public void setText(String text) {
setState(STATE_NORMAL);
mText = text;

recalculate();
invalidate();
}

public String getText() {
return mText;
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);

mWidth = w;
mHeight = h;

mBackgroundRect.left = 0;
mBackgroundRect.top = 0;
mBackgroundRect.right = w;
mBackgroundRect.bottom = h;

recalculate();
invalidate();
}

public void setState(int state) {
mState = state;
invalidate();
}

private void recalculate() {
mTextPaint.setTextSize(mTextSize);

mTextPaint.getTextBounds(mText, 0, mText.length(), mTextBounds);
if(mWidth != 0) {
while (mTextBounds.width() >= mWidth - mLeftRightPadding * 2) {
mTextPaint.setTextSize(mTextPaint.getTextSize() - 2);
mTextPaint.getTextBounds(mText, 0, mText.length(), mTextBounds);
}
}

mTextLeftX = (mWidth - mTextBounds.width()) / 2 - mTextBounds.left;
mTextTopY = (mHeight - mTextBounds.height()) / 2 - mTextBounds.top;
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

if(mState == STATE_NORMAL) {
canvas.drawBitmap(sBackgroundBitmap, null, mBackgroundRect, mBackgroundPaint);
} else if(mState == STATE_PRESSED) {
canvas.drawBitmap(sPressedBitmap, null, mBackgroundRect, mBackgroundPaint);
} else if(mState == STATE_SUCCESS) {
canvas.drawBitmap(sSuccessBitmap, null, mBackgroundRect, mBackgroundPaint);
} else {
canvas.drawBitmap(sFailedBitmap, null, mBackgroundRect, mBackgroundPaint);
}
canvas.drawText(mText, mTextLeftX, mTextTopY, mTextPaint);
}
}


Анімація
Статичний Splash-screen виглядав нудно. Тому вирішили додати до нього анімацію, а саме обертовий британський прапор в буквою g логотипу. Можна було зробити звичайну gif-анімацію, але ми легких шляхів не шукаємо, тому все реалізовано в коді.

Реалізована анімація була досить просто: в розмітці першим шаром йде зображення британського прапора, вирівняні по центру відносно контейнера, наступним шаром йде зображення слова funglish, теж вирівняні по центру. При відкритті Activity запускається анімація, яка виконує 2 обороту зображення британського прапора. Також було змінено стандартний інтерполятор анімації на AccelerateDecelerateInterpolator, щоб обертання нелінійно прискорювалося і сповільнювався.

Анімація вийшла досить симпатичною і запам'ятовується і сподобалася всім опитаним людям.

Tutorial
Більшість статей і книг про розробку і просування додатків в один голос твердять, що одним з найважливіших моментів є навчання користувача використанню продукту. Для навчання був створений екран Підручник, у якому на 3-х сторінках користувачеві коротко розповідаються ключові моменти гри. Даний екран з'являється один раз перед першим початком гри.

СкріншотTutorial

Опублікувати

Публікація програми була зроблена 7 серпня. Думаю, не має сенсу описувати процес отримання ключа і викладання програми на Play Market.

Додаток доступно для скачування лише в кількох країнах, де можна зустріти велику кількість російськомовного населення, це, звичайно ж, Росія і Білорусь, Болгарія, Казахстан і Україна. Як ви вже зрозуміли, у програмі є підтримка тільки російської мови.

Тестування

Коли велика частина гри була зроблена, контент був знайдений, а гра вже готувалася до релізу, було проведено тестування. Для цього були знайдені добровольці, які погодилися пограти в альфа-версію гри.

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

При цьому був виявлений ефект «Flappy Bird», коли користувач не встигає до закінчення таймера відповісти на слова або в останній момент відповідає неправильно – він дуже злиться. Емоції, хоч у даному випадку вони негативні, несуть позитивний ефект. Користувач не може заспокоїтися, поки не пройде рівень, а значить, вивчить незнайоме йому слово.

Поширення

З деяких причин у мене не було можливості впритул зайнятися поширенням програми. Єдине, що було зроблено в перший день – викладання посилання в соц. мережа ВКонтакте, і розповідь друзям з проханням завантажити додаток. Перший день приніс 17 скачувань. Потім кількість скачувань впало до 1 на день.

20 серпня була створена тема на форумі сайту 4pda.ru, де було викладено опис програми та посилання на Play Market. На сьогоднішній день цю тему подивилися понад 700 осіб. Трохи пізніше на сайті r-android.ru приємно здивував і написав огляд гри на своєму сайті. У ці дні кількість скачувань піднялося до 30.
Статистика скачуваньСтатистика скачувань

Треба зазначити, що 4pda.ru приніс велику аудиторію користувачів. І, швидше за все, автори сайту r-android.ru саме звідти дізналися про гру і написали на неї огляд.

Висновок

Якщо озирнутися назад, можна зрозуміти, що багато чого було зроблено не так або не вчасно. Наприклад, можна було краще продумати концепцію програми, щоб на середині розробки дизайну не переробляти половину роботи дизайнера.

Тестування UX можна і потрібно було провести в першу чергу на швидко зібраному концепті, щоб перевірити ідею. Але це було зроблено вже перед самим релізом.

Із-за відсутності часу, не став додавати статистику Flurry, хоча зараз вона дала б дуже багато корисної та необхідної інформації про використання програми.

Також дуже хотілося додати рекламу AdMob, щоб подивитися які доходи він може принести і принесе. Але знову не вистачило часу.
До релізу теж не був: не було написано ні прес-релізи, ні статті, нічого. Якщо би краще підійшов до цього питання, в день викладання програми можна було б поширити повідомлення на тематичних форумах, зробити сторінку програми в соц.мережах, розіслати статті на різні сайти. Все це могло б в теорії принести велику кількість скачувань, що позитивно позначилося б на становищі програми в рейтингу Play Market'а.

Тема вивчення англійської мови є дуже актуальною, тому зараз стою перед вибором: зробити невелике оновлення (оновити контент), зробити концептуальне оновлення (додати нові види рівнів, наприклад аудіо-переклад), або зробити пакет програм для вивчення англійської мови (часи, неправильні дієслова та ін).

Кажуть, «перший млинець – комом». Але думаю, що до Funglish це не відноситься. Розробка цієї гри – безцінний досвід. Гра має хороші рейтинги, люди пишуть відгуки, просять додати деякі нові можливості. Дуже приємно усвідомлювати, що комусь дійсно подобається твоє додаток. Це надихає робити нові ігри і програми.

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

0 коментарів

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