Pull to refresh

Портирование игры из реального мира на Android

Reading time8 min
Views9.1K
Началось всё как в самом настоящем детективе: новогодние праздники, 31 декабря, родительский дом за много километров от москвы и что самое страшное — полное отсутствие интернета и телеканала 2x2. Мозг может работать в двух режимах — либо потреблять контент, либо создавать его. Так получилось, что у меня мозг в тот момент заработал во втором режиме. По случайному стечению обстоятельств, мне на глаза попалась давно забытая игра-головоломка «Пифагор»:



И я решил «оцифровать» её.



Головоломка Пифагор состоит из 7-ми геометрических фигурок, которые необходимо сложить таким образом, чтобы получилась фигура, указанная в задании. При решении задания необходимо использовать все 7 фигурок головоломки.

Выбор платформы.

Ну и в довесок ко всему, я давно хотел написать что-нибудь под «мобильник».

Так как на ноуте стоял Eclipse с необходимыми приблудами для Android, и было несколько книжек про него, скачанных «про запас», то выбор был предопределён.

Итак, решено: будем писать игрушку «Пифагор» для Android. Так же отмечу, что я не являюсь программистом, в моём понимании этого слова.

Реализация идеи

Не буду описывать как устанавливать и настраивать Eclipse и как создавать проект, так как про это и так много всего написано, наконец-то перейду к сущности повествования.

Создаём проект с именем Pifagor.
После создания у нас будет файл с именем Pifagor.java и содержимым:

package Pifagor.android.game;

import android.app.Activity;
import android.os.Bundle;

public class Pifagor extends Activity {
    /** Called when the activity is first created. */
	
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        pifagorView = (PifagorView) findViewById(R.id.game);
    }   
}


Начало положено.
Теперь создадим файл PifagorDraw.java в котором и будет реализована наша игра. Определим класс PifagorDraw который дочерним для класса Thread. Для нашего класса определим методы обработки событий он экрана, сброс игры в начальное состояние и переопределим метод run:

package Pifagor.android.game;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.view.MotionEvent;
import android.view.SurfaceHolder;

public class PifagorDraw extends Thread {
	private SurfaceHolder appScreen;
	private boolean appRunning;			// флаг работы потока
	private boolean appPaused;			// флаг паузы потока
	private Paint appPaint;				// 
	private Bitmap clearScreen;			// битмап для очистки экрана
	private PifagorPuzzle Puzzles[];	// массив фигур
	private int activePuzzle;			// номер активной фигуры
	private long thisTime;				// переменная для сохранения времени нажатия на экран
	private long downTime;				// переменная для сохранения времени отжатия от экрана
	private PifagorTask Tasks[];		// массив заданий
	private PifagorTask ResetPiktogram;	// пиктограмма для кнопки сброса
	private int activeTask;				// номер активного задания
	private int maxTasks;				// общее количество заданий
	private boolean prevButton, nextButton, resetButton;	// флаги нажатия кнопок

// Описание фигур
	// большой квадрат
	private int PuzzleTemplate1[][][] = {......};
	// большой треугольник
	private int PuzzleTemplate2[][][] = {......};
	// малый квадрат
	private int PuzzleTemplate3[][][] = {......};
	// малый треугольник
	private int PuzzleTemplate4[][][] = {......};
	// косой паралепипед
	private int PuzzleTemplate5[][][] = {......};

// Пиктограмма кнопки "Сброс"
	private int Pikto1[][] = {......};
	
// Описание заданий
	private int Task1[][] = {......};
	private int Task2[][] = {......};
	private int Task3[][] = {......};
	private int Task4[][] = {......};

// Конструктор
	public PifagorDraw(SurfaceHolder surfaceHolder, Context context) {
		// определение начальных значений переменных 
		appScreen = surfaceHolder;
		appRunning = false;
		appPaused = false;
		activePuzzle = 0;
		thisTime = 0;
		downTime = 0;
		prevButton = false;
		nextButton = false;
		resetButton = false;

                // Стиль рисования
		appPaint = new Paint();
		appPaint.setColor(Color.BLUE);
		appPaint.setStrokeWidth(2);
		appPaint.setStyle(Style.STROKE);
		
		// создание экрана для очистки изображения
		clearScreen = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.RGB_565);
		
		// определение и заполнение массива фигурок
		Puzzles = new PifagorPuzzle[7];
		Puzzles[0] = new PifagorPuzzle(......);
		Puzzles[1] = new PifagorPuzzle(......);
		Puzzles[2] = new PifagorPuzzle(......);
		Puzzles[3] = new PifagorPuzzle(......);
		Puzzles[4] = new PifagorPuzzle(......);
		Puzzles[5] = new PifagorPuzzle(......);
		Puzzles[6] = new PifagorPuzzle(......);
		PuzzlesReset();
		
		// определение и заполнение массива заданий
		maxTasks = 4;
		activeTask = 0;
		Tasks = new PifagorTask[maxTasks];
		Tasks[0] = new PifagorTask(......);
		Tasks[1] = new PifagorTask(......);
		Tasks[2] = new PifagorTask(......);
		Tasks[3] = new PifagorTask(......);
		
		// создание пиктограммы сброса
		ResetPiktogram = new PifagorTask(......);
	}
	
// установка флага работы процесса	
	public void setRunning(boolean status) {
		appRunning = status;
	}
	
// обработка сбытий от экрана
	public boolean Touch(MotionEvent event) {
		int mouseX = (int)event.getX();
		int mouseY = (int)event.getY();
		switch(event.getAction()) {
			case MotionEvent.ACTION_MOVE:					// обработка движения
			        ......
				break;
			case MotionEvent.ACTION_DOWN:				       // обработка нажания
				......
				break;
			case MotionEvent.ACTION_UP:		                	// обработка отпускания
				......
				break;
		}
		return true;  // возвращаем true, потому что события от экрана отработаны
	}
	
// Функция сброса в игры в начальное состояние 	
	public void PuzzlesReset() {
		......
	}
	
	@Override
	public void run() {
		while(appRunning) {
			if (!appPaused) {
				Canvas canvas = null;
				try {
					canvas = appScreen.lockCanvas();
					synchronized(appScreen) {
						
                                                // здесь прописываем логику игры и отрисовку игровых объектов
                                                // Например:

                                                // очистка изображения
						canvas.drawBitmap(clearScreen, 0, 0, null);
					
						// рисование фигур задач
						for(int i=0;i<7;i++) {
							Puzzles[i].Update();
							Puzzles[i].Draw(canvas);
						}	
						
                                                ......
                                                
						sleep(20);
					}
				} catch (Exception e) { }
				finally {
					if (canvas != null) { appScreen.unlockCanvasAndPost(canvas); }
				}
			}
		}
	}
}


Я прочитал про различные способы работы с графикой на Android, но более-менее пока разобрался с одним, его и буду использовать.

Теперь создадим в проекте файл с именем PifagorView.java. В этом файле объявляем класс PifagorView, который является дочерним для класса SurfaceView и добавляем наследование интерфейса SurfaceHolder.Callback — этот класс будет связующим звеном.
В созданном классе нам необходимо переопределить три унаследованных от SurfaceView метода для реагирования на изменения состояния View, а так же один метод для обработки событий от экрана(нажатие, перетаскивание, отпускание):

package Pifagor.android.game;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class PifagorView extends SurfaceView implements SurfaceHolder.Callback {

	private SurfaceHolder appScreen; 
	private PifagorDraw appThread;
	
	public boolean surfaceCreated;
	
// конструктор
	public PifagorView(Context context, AttributeSet attrs) {
		super(context, attrs);
		surfaceCreated = false;
		appScreen = getHolder();
		appScreen.addCallback(this);
		
		appThread = new PifagorDraw(appScreen, context);
		setFocusable(true);		
	}


	@Override
	public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
		// TODO Auto-generated method stub		
	}

	@Override
	public void surfaceCreated(SurfaceHolder holder) {
		appThread = new PifagorDraw(holder, getContext(), screenWidth, screenHeight);
		appThread.setRunning(true);
		appThread.start();
	}

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

        @Override
	public boolean onTouchEvent(MotionEvent event) {
		return appThread.Touch(event); // так как вся логика игры у нас прописана в классе PifagorDraw, то мы просто перенаправляет туда полученные события
	}
}


Теперь нам надо подправить файл main.xml — мы создали наш объект View и теперь его надо расположить на экране:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
	<Pifagor.android.game.PifagorView
		android:id="@+id/game"
		android:orientation="vertical"
		android:layout_width="fill_parent"
		android:layout_height="fill_parent"
	/>
</LinearLayout>


Так же в проекте есть ещё два класса PifagorPuzzle, в который описывает геометрические фигурки головоломки, и класс PifagorTask, описывающий задания. Их рассматривать не будем.

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

Во-первых, в классе PifagorView создадим методы CreateThread и TerminateThread (из названия понятно для чего они будут использоваться), и несколько изменим переопределённые методы SurfaceCreated и SurfaceDestoryed:
public class PifagorView extends SurfaceView implements SurfaceHolder.Callback {

	private SurfaceHolder appScreen; 
	private PifagorDraw appThread;
	
	public boolean surfaceCreated;
	
	private int screenWidth;
	private int screenHeight;
	
// конструктор
	public PifagorView(Context context, AttributeSet attrs) {
		super(context, attrs);
		surfaceCreated = false;
		appScreen = getHolder();
		appScreen.addCallback(this);
		
		appThread = new PifagorDraw(appScreen, context, screenWidth, screenHeight);
		setFocusable(true);		
	}

	
// уничтожение процесса
	public void terminateThread() {
		boolean retry = true;
		appThread.setRunning(false);
		while(retry) {
			try {
				appThread.join();
				retry = false;
			} catch (InterruptedException e){ }
		}	
	}
	
// создание процесса
	public void createThread(SurfaceHolder holder) {
		appThread = new PifagorDraw(holder, getContext());
		appThread.setRunning(true);
		appThread.start();
	}

	@Override
	public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
		// TODO Auto-generated method stub		
	}

	@Override
	public void surfaceCreated(SurfaceHolder holder) {
		if (surfaceCreated == false) {
			createThread(holder);
			surfaceCreated = true;
		}
	}

	@Override
	public void surfaceDestroyed(SurfaceHolder arg0) {
		terminateThread();
	}
	
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		return appThread.Touch(event);
	}
}


Во-вторых в классе Pifagor переопрделим методы onPause и onResume:
public class Pifagor extends Activity {
    /** Called when the activity is first created. */
	
	private PifagorView pifagorView;
	
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        pifagorView = (PifagorView) findViewById(R.id.game);
    }
    
    @Override
    public void onPause() {
    	super.onPause();
    	pifagorView.terminateThread();
    }
    
    @Override
    public void onResume() {
    	super.onResume();
    	if (pifagorView.surfaceCreated == true)
        {
            pifagorView.createThread(pifagorView.getHolder());
        }
    }
}


Таким образом, при сворачивании приложения, игровой процесс описанный в классе PifagorDraw будет уничтожаться, а при возврату к приложению, запускаться вновь.

Полные исходники приводить не стал, если кому интересно, пишите в личку, вышлю.

Ну что ж, запускаем и смотрим что получилось:

Дизайнера для игрушки так и не нашёл. Если кто-то готов поучаствовать — пишите в личку.

Что дальше?
Игра написана, надо что-то делать дальше. Или убирать в архив, где она сгинет или попробовать выставить в Android Maket. Второе намного интереснее, по этому регистрируемся на market.android.com/publish, регистрируем Google Checkout (лучше регистрировать через IE, с остальными браузерами у меня были проблемы), платим 25$ и как обычно в самый последний момент пытаемся понять куда мы опять вляпались, то есть ищем информацию — как продвигать приложения в Android Market. Читаем статью и после этого составляем следующий план:
  • Публикуем бесплатную демо-версию игры, содержащую всего 5 заданий
  • Публикуем платную версию игры, с 30 заданиями
  • Публикуем обновление игры с некоторыми доработками
  • Пишем обзоры игры на различных интернет-ресурсах
  • Публикуем обновлённую версию игры с дополнительными 30 заданиями

    А так же параллельно составляем список функций, которые хотелось бы добавить в игру.

    Так как данная статья является реализацией третьего пункта, то поделюсь результатами первых двух:
    Демо-версию игры скачали чуть более 200 человек, активных установок около 100, что для первого приложения, на мой взгляд очень неплохо. Так же получил несколько советов по игре.

    Через неделю, после публикации демо-версии, опубликовал первую редакцию платной версии игры. Демо-версия была удалена — из-за того что потерял сертификат, которым подписывал приложение, и не смог загрузить обновление демо-версии — так что будьте аккуратнее. За первые 24 часа нахождения игры в маркете её купили 2 человека, причём оба из штатов, судя по профилю в google checkout. В течении недели была ещё одна продажа.

    Следуя плану, доработал и выложил обновлённую версию игры с дополнительными фичами.

    Как будут развиваться события дальше, отпишусь позднее.

    В заключении...
    Времени потраченного на всё это абсолютно не жаль, так как получил определённый опыт и много удовольствия. Так что если кто ещё раздумывает, начать или нет, ребята, не раздумывайте! Дерзайте! Это того стоит!

    P.S. В планах портировать игру под Windows Phone 7 и если не пропадёт запал то и под iOS.
Tags:
Hubs:
+48
Comments36

Articles

Change theme settings