Хочется иногда взять и отдохнуть от всех этих фреймворков, движков и готовых библиотек. Точнее, не отдохнуть, а напрячься — взять и накодить какую-нибудь игрушку исключительно с помощью стандартных средств. Какую именно? Выбрать легко, ведь в сердце любого программера неизменно живут три игры, созданные в прошлом веке: Tetris, Digger и Xonix.

Кстати, эту статью имеет смысл читать даже в том случае, если ты не планируешь вдыхать новую жизнь в игры нашего детства, ведь настоящая цель этого материала — показать тебе работу с холстом, шрифтами, графическими объектами, потоками, стандартной архитектурой приложений и наследованием.

Xonix — ну кто его не знает?
Xonix — ну кто его не знает?
 

Инструментарий и логика

Разработку будем вести в рекомендованной самой компанией Google Android Studio. Все, что нам понадобится, легко найти по ссылке.

Создадим пустой проект и все классы будем добавлять вручную. Вся игра будет состоять из пяти классов активити, четыре из которых мы опишем в манифесте, а пятый будет родителем для всех предыдущих, то есть все классы активити будут наследоваться от одной — так получится меньше кода. Давать старт приложению будет SplashActivity, она после временной задержки вызовет MainMenuActivity, оттуда можно будет зайти в настройки SettingsActivity или запустить основную часть игры, расположенную в GameActivity. Все активити (кроме последней) будут реализовывать свой интерфейс самым стандартным из всех способом, а именно описанием элементов в XML файлах разметки (layout).

Основное игровое поле нарисуем на наследнике класса SurfaceView. Отрисовка будет выполняться в параллельном основному UI-потоке. Сейчас рассмотрим тот класс, который не попал в манифест, но сэкономит нам много кода и времени.

 

Класс BaseActivity

Чтобы экран устройства не выключался во время работы приложения, создадим класс BaseActivity и от него унаследуем все Activity приложения:

public class BaseActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Убрали заголовок
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        // Убрали отключение экрана и выставили полноэкранный режим
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
            | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
            WindowManager.LayoutParams.FLAG_FULLSCREEN);
        // Задали портретную ориентацию
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        // Задали анимацию при запуске активности
        overridePendingTransition(android.R.anim.slide_in_left, android.R.anim.slide_out_right);
    }
}

Этот класс обеспечит нас активностью без заголовка, с портретной ориентацией, не гаснущим из-за экономии энергии экраном и анимацией появления каждой новой активности.

 

Окно приветствия

Компания Google не рекомендует использовать окно приветствия (Splash Screen) в приложениях. Но рекомендация не значит запрет, мне очень хочется показать, как оно делается, поэтому мы реализуем его при помощи простого класса SplashActivity:

public class SplashActivity extends BaseActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_splash);
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                startActivity(new Intent(SplashActivity.this, MainMenuActivity.class));
                finish();
            }
        }, 2000);
    }
}

Здесь мы просто показываем нашу разметку setContentView(R.layout.activity_splash). По истечении 2000 мс запустим MainMenuActivity, а саму ее выключим методом finish(). Нужно заметить, что окно приветствия бывает полезно, когда нужно сделать подготовку для работы приложения, например скопировать БД или быстро что-то скачать из Сети.

Ну а в это время пользователю следует показать красивый логотип — как это ни парадоксально, среднему юзеру такое ожидание даже нравится, у него создастся впечатление, что приложение серьезное, раз оно что-то долго просчитывает перед запуском. Этим пользуются некоторые разработчики: показывают перед запуском крутящиеся колесики (ProgressBar), хотя ничего полезного в этот момент не делается.

Чтобы навести некоторую красоту и самобытность, мы можем использовать шрифты TTF и OTF. Применять их можно двумя способами:

  1. Динамически, то есть во время работы приложения устанавливать нужному элементу нужный шрифт методом setTypeface().
  2. Статически — переопределив стандартный компонент отображения и прописав его в файле разметки.

В нашем приложении будут реализованы оба способа. Положим файл шрифта Dots.ttf в папку assets. Разметку первых двух экранов реализуем статически. Для этого создадим класс MyTextView, наследника от стандартного TextView. Зададим ему стиль в файле attrs.xml .

Основной метод класса init():

private void init(AttributeSet attrs) {
    if (attrs != null) {
        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.MyTextView);
        String fontName = a.getString(R.styleable.MyTextView_fontName);
        if (fontName != null) {
            Typeface myTypeface = Typeface.createFromAsset(getContext().getAssets(), fontName);
            setTypeface(myTypeface);
        }
        a.recycle();
    }
}

В разметке activity_splash.xml пропишем наш класс:

<com.rusdelphi.xonix.MyTextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerHorizontal="true"
    android:layout_centerVertical="true"
    android:text="@string/app_name"
    android:textSize="40sp"
    android:textStyle="bold"
    customfont:fontName="Dots.ttf" />

Применим свой шрифт динамически, во время рисования текста на игровом поле (рис. 1).

Рис. 1. Вид экрана окна приветствия
Рис. 1. Вид экрана окна приветствия

В основном меню реализуем три кнопки: саму игру, настройки и другие приложения. Начнем с последнего. Для того чтобы дать пользователю возможность насладиться другими приложениями автора, достаточно прописать одну строчку — поисковый запрос с именем автора:

startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://search?q=pub:Василий Пупкин")));

То есть запускаем программу через определенные намерения.

Рис. 2. Вид экрана главного меню
Рис. 2. Вид экрана главного меню

Приложение должно обрабатывать Uri со строкой market. Система сама определяет по описанию в манифестах, какие приложения смогут обработать такое намерение, и, если их несколько, предложит сделать выбор (рис. 3).

Рис. 3. Выбор маркета
Рис. 3. Выбор маркета

Перед публикацией приложения в маркете

Не забудь воспользоваться хорошим инструментом для очистки проекта, который входит в Android Studio Имя ему Analyze — Inspect Code. В результате ты получишь список найденных замечаний в проекте — от неиспользуемых ресурсов до ненужных переменных. Учти эти замечания, оптимизируй проект!

В настройках (SettingsActivity) используется стандартный механизм SharedPreferences. Все настройки вынесены в отдельный класс Preference. При старте активности (onStart()) с настройками, мы загружаем нужные настройки в контролы. Если мы изменили настройки, то они сохранятся при выходе из активности (onStop()). В настройках мы указываем скорость игры и количество жизней при старте игрового процесса.

Игровая активность GameActivity также является наследником BaseActivity. От родителя она унаследовала все описанные выше полезные свойства, так что нам остается только реализовать саму игру. Во время создания этой активности мы вместо привычного указания XML-файла с разметкой элементов (layout) укажем для экрана собственный класс DrawView, который является наследником SurfaceView:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    self = this;
    ctx = this;
    mDrawView = new DrawView(this);
    setContentView(mDrawView);
    DisplayMetrics dimension = new DisplayMetrics();
    getWindowManager().getDefaultDisplay().getMetrics(dimension);
    int width = dimension.widthPixels;
    int height = dimension.heightPixels;
    if (width < height)
        mSidePopup = (int) (width * 0.8);
    else
        mSidePopup = (int) (height * 0.8);
}

Тут же мы получаем параметры экрана (wight,height), которые будем использовать при создании всплывающего окна (PopupWindow) с сообщением об окончании игры.

 

Класс SurfaceView

SurfaceView — обертка вокруг класса SurfaceHolder, который, в свою очередь, служит оберткой класса Surface, используемого для обновления изображения из фоновых потоков. Особенность класса SurfaceView заключается в том, что он предоставляет отдельную область для рисования, действия с которой должны быть вынесены в отдельный поток приложения. Таким образом, приложению не нужно ждать, пока система будет готова к отрисовке всей иерархии View-элементов. Вспомогательный поток может использовать холст (Сanvas) нашего SurfaceView для отрисовки с той скоростью, которая необходима.

Вся реализация сводится к двум основным моментам:

  1. Создание класса, унаследованного от SurfaceView и реализующего интерфейс SurfaceHolder.Callback.
  2. Создание потока, который будет управлять отрисовкой.
 

Класс Canvas

Класс Canvas предоставляет методы для рисования, которые отображают графические примитивы на исходном растровом изображении. При этом надо сначала подготовить кисть (класс Paint), который позволяет указывать, как именно графические примитивы должны отображаться на растровом изображении (цвет, обводка, стиль, сглаживание шрифта и так далее). Также нужно указать Bitmap — поверхность, на которой происходит рисование. Android поддерживает полупрозрачность, градиентные заливки, округленные прямоугольники и сглаживание. Из-за ограничения ресурсов векторная графика пока что не поддерживается, вместо этого используется традиционная растровая перерисовка.

Класс Canvas можно назвать оберткой вокруг растрового изображения, которое мы будем использовать в качестве полотна для своих художественных опытов. Он предоставляет набор методов вида draw* для создания изображений котов и других объектов.

 

Класс DrawView

В классе DrawView мы будем отлавливать события нажатия на экран (onTouchEvent) и запускать рабочий поток DrawThread, реализующий всю игровую логику и работающий с холстом. Монстр в игре движется сам, а вот игроком нужно управлять. Путем нехитрых вычислений определим четыре жеста:

@Override
public boolean onTouchEvent(MotionEvent touchevent) {
    switch (touchevent.getAction()) {
        // Определяем координаты первого касания
        case MotionEvent.ACTION_DOWN: {
            x1 = touchevent.getX();
            y1 = touchevent.getY();
            break;
        }
        case MotionEvent.ACTION_UP: {
            x2 = touchevent.getX();
            y2 = touchevent.getY();
            float dx = x2 - x1;
            float dy = y2 - y1;
            if (Math.abs(dx) > Math.abs(dy)) {
                if (dx > 0)
                    DrawThread.playerDirection = "right";
                if (dx < 0)
                    DrawThread.playerDirection = "left";
            } else {
                if (dy > 0)
                    DrawThread.playerDirection = "down";
                if (dy < 0)
                    DrawThread.playerDirection = "up";
            }
        }
        break;
    }
    return true;
}

В методе surfaceCreated() получим элементы для холста и запустим поток отрисовки, а в surfaceDestroyed(), наоборот, завершим его:

@Override
public void surfaceCreated(SurfaceHolder holder) {
    Preference prefs = new Preference(getContext()); // Получили настройки
    int indent = Tools.dpToPx(5); // Отступ между элементами
    int side = (getWidth() / 40) - indent; // Размер квадрата
    int startY = (getHeight() - (side + indent) * 20) / 2;
    int startX = 5;
    int i, j;
    for (i = 0; i < 40; i++)
        for (j = 0; j < 20; j++) {
            int x1 = startX + side * i + indent * i;
            int y1 = startY + side * j + indent * j;
            int x2 = startX + side * i + side + indent * i;
            int y2 = startY + side * j + side + indent * j;
            if (i == 0 || i == 39 || j == 0 || j == 19)
                matrixField[i][j] = new QuadrateItem(x1, y1, x2, y2, Color.BLUE);
            else
                matrixField[i][j] = new QuadrateItem(x1, y1, x2, y2, Color.TRANSPARENT);
        }
    Activity activity = (Activity) getContext();
    drawThread = new DrawThread(getHolder(), getResources(), matrixField,prefs.getData(Preference.GAME_SPEED),prefs.getData(Preference.NUMBER_OF_LIFES),activity);
    drawThread.setRunning(true);
    drawThread.start();
}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
    boolean retry = true;
    drawThread.setRunning(false);
    while (retry) {
        try {
            drawThread.join();
            retry = false;
        } catch (InterruptedException e) {
        }
    }
}

Наше игровое поле состоит из верхней надписи и матрицы квадратов 40 х 20, в которой и будет разворачиваться вся игровая драма.
Все рисование выполняется на холсте, то есть на классе Canvas. В потоке мы получаем его:

canvas = surfaceHolder.lockCanvas(null);

Рисуем на нем:

canvas.drawColor(Color.BLACK);

или:

public void drawRect(Canvas canvas, QuadrateItem item) {
    Rect myRect = new Rect();
    myRect.set(item.x1, item.y1, item.x2, item.y2);
    Paint itemPaint = new Paint();
    itemPaint.setColor(item.color);
    itemPaint.setStyle(Paint.Style.FILL);
    canvas.drawRect(myRect, itemPaint);
}

и возвращаем обновленным:

surfaceHolder.unlockCanvasAndPost(canvas);

Саму логику игры, думаю, описывать излишне. Если количество жизней стало меньше единицы, то покажем Game Over — экран окончания.

Рис. 3. Экран окончания
Рис. 3. Экран окончания
 

Заключение

В наше время существует много инструментов разработки игр для Android, в том числе и онлайн-редакторы. Работа их сводится к оборачиванию JavaScript-кода в приложение с компонентом WebView, то есть в обычный браузер. Нужно отметить, что сам JS в ОС Android урезан, дальше песочницы браузера он выйти не может и, следовательно, к многим функциям самого устройства доступа не имеет. Не говоря уже об ошибках браузера, который обновляется только на последних версиях ОС. В общем, о подобных поделках в приличном обществе лучше помолчать. Работа с фреймворками накладывает ошибки программиста на ошибки самой платформы, что в итоге приводит к множеству проблем, зачастую даже неразрешимых. Поэтому, если ты хочешь сделать хорошую игру, нужно держаться ближе к самой платформе и брать стандартные инструменты (более чем спорное утверждение, но звучит брутально. — Прим. ред.).

Зарабатываем денежки

Есть несколько способов монетизации своих трудов:

  1. Встроить рекламу и получать деньги за клики или просмотры.
  2. Продавать приложения.
  3. Встроить покупки в приложения.
  4. Шпионить за пользователем и отправлять узнанное «куда надо» (см. Angry Birds).
  5. Комбинация вышеперечисленного.

Обычно опенсорсники реализуют кнопочку с пожертвованиями, но пожертвования при этом должны проходить через Google, иначе он не получит свои 30% , обидится и забанит приложение в маркете. Есть два варианта такой реализации — предложить платную версию приложения или реализовать покупку внутри приложения (In-app Billing). Для платной версии просто указываем ссылку на платную версию в маркете. Для «встроенных покупок» есть механизм In-app Billing.

Через «встроенную покупку» в игре можно добавлять игровые бонусы, отключать рекламу или просто жертвовать на пиво, но отслеживать покупки надо через свой сервер, так как механизм Google уже давно взломали приложениями типа Freedom. Сами покупки регистрируются в консоли разработчика в разделе «Контент для продажи». За более подробной информацией по In-app Billing API добро пожаловать сюда, а здесь есть хорошие примеры с кодом.

2 комментария

  1. Аватар

    zabr

    12.08.2015 в 12:25

    ребята я немного туговат, можно ссылку на исходники проекта?

Оставить мнение