Создание простой игры для Android на AIR. Часть вторая

1
Часть первая

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

Вибрация.


Сделаем так, чтобы при попадании красного айтема на платформу срабатывала короткая вибрация. Но вот проблема — у AIR нет доступа к API вибрации. На помощь приходит замечательная вещь под названием Native Extension. Native Extension — это средство для взаимодействия ActionScript 3 кода с собственным кодом разработки для платформы, на которой запускается приложение. Для Android — это Java. На этом языке разрабатываются нативные приложения для андроида. Каждое расширение(extension) состоит из двух частей — собственного кода платформы и AS3 кода. Всё это упаковывается в файл с расширением ane, который подключается к проекту. Также расширение имеет идентификатор, который нужно прописать в application.xml. Таким образом Native Extension позволяет получить доступ к API операционной системы через ActionScript 3. Выходит, что нам нужно написать расширение на Java и AS3 код. В случае с вибрацией этого делать не придётся, так как добрые разработчики из Adobe уже написали это расширение. Надо только им воспользоваться.
По ссылке содержится инструкция и архив с расширением, его исходниками и примеры использования.

Качаем архив, распаковываем, идём в папку VibrationNEDeliverables/ReadyToUseExtension. Там лежат три файла:
com.adobe.extensions.Vibration.ane — библиотека расширения;
VibrationActionScriptLibrary.swc — библиотека с as3 классами;
extensionID.txt текстовый файл с идентификатором расширения.

Кладём первые два файла (библиотеки) в папку проекта lib. Выделяем их, жмём правую кнопку/Add To Library.
Далее правый клик по com.adobe.extensions.Vibration.ane/Options и выбираем третий пункт External library (not included).



Путь к расширению нужно прописать в файле bat/Packager.bat. Этим путём является папка lib.
Открываем Packager.bat, находим строку, которая начинается на call adt. Дописываем в конце этой строки " -extdir lib/"
Например, если была строка:
call adt -package -target %TYPE%%TARGET% %OPTIONS% %SIGNING_OPTIONS% "%OUTPUT%" "%APP_XML%" %FILE_OR_DIR%
то должна получиться такая:
call adt -package -target %TYPE%%TARGET% %OPTIONS% %SIGNING_OPTIONS% "%OUTPUT%" "%APP_XML%" %FILE_OR_DIR% -extdir lib/

Теперь нужно записать идентификатор расширения в application.xml. Можно сделать это через настройки проекта.
Открываем Project/AIR App Properies, переходим к вкладке Extension и добавляем идентификатор нашего расширения:



Также нужно дать разрешение(permission. Если вы разрабатываете под андроид, то знаете, что это такое) приложению на вибрацию. Делается это на вкладке Mobile Additions/Android Manifest Additions.
Там нужно добавить строку:
<uses-permission android:name="android.permission.VIBRATE" />



Это тот случай, когда подготовка занимает больше времени, чем написание кода.
Переходим в класс GameScreen, создаём там новую переменную:
/** @private переменная вибрации */
  private var vibration:Vibration = new Vibration();

Идём в функцию updateGame и в случае, когда пойман злой айтем, запускаем вибрацию:
//...
if (tempItem.hitTestObject(platform)) {
     switch (tempItem.type) {
      case ItemType.GOOD:
       currentScore++;
      break;
      
      case ItemType.VERY_GOOD:
       currentScore +=5;
      break;
      
      case ItemType.EVIL:
       currentScore--;
       // вибрируем 150 миллисекунд, если вибрация вообще поддерживается
       if (Vibration.isSupported) vibration.vibrate(150);
      break;
     }
     gameContainer.removeChild(tempItem);
    }
//...

Всего две строчки кода. Можно тестировать на устройстве, ловить красные айтемы и ощутить вибрацию.

Управление акселерометром.


Теперь мы хотим добавить новый тип управления — наклоном телефона. Практически во всех мобильных девайсах есть устройство под названием акселерометр. Он позволяем определять силу, приложенную к девайсу. Можно определить положение (наклоны) устройства в пространстве по трём осям. Нам нужна только ось .x. Если взять смартфон перед собой дисплеем к себе и наклонить его против часовой стрелки на 90 градусов, акселерометр выдаст значение 1, при таком же повороте по часовой стрелке значение будет равно -1. Нам нужно работать с интервалом от 0.5 до -0.5. Попробую изобразить это на схеме. Вид спереди:



Вид сверху или снизу:
(то есть смартфон «лежит» дисплеем вверх, а мы видим его нижнюю грань)


Native Extension для работы с акселерометром не требуется, так как для этого есть ActionScript средства, а именно класс Accelerometer.
В игру нужно добавить новый тип управления — наклоном устройства и при этом сохранить старый — пальцевый. Мы создадим экран настроек, который позволит выбрать тип управления.
Обо всём по порядку. Для начала нужны константы для определения типа управления:
Создадим в пакете constants класс ControlType
package constants {
	
	/**
	 * Статические константы для определения типа управления
	 * 
	 * @author illuzor
	 */
	
	public class ControlType {
		/** управление пальцем */
		public static const TOUCH_CONTROL:String = "touchControl";
		/** управление наклоном устройства */
		public static const SENSOR_CONTROL:String = "sensorControl";
		
	}
}

В корне создадим класс Settings для хранения выбранного типа управления. Он содержит статическую переменную типа управления для доступа из любой части приложения
package  {
	
	import constants.ControlType;
	
	/**
	 * Класс для хранения настроек
	 * 
	 * @author illuzor
	 */
	
	public class Settings {
		/** тип управления */
		public static var controlType:String = ControlType.TOUCH_CONTROL;
		
	}
}

Добавим в класс ScreenType константу для экрана настроек SETTINGS_SCREEN
package constants {
	
	/**
	 * Статические константы для определения типа экрана
	 * 
	 * @author illuzor
	 */
	
	public class ScreenType {
		/** главное меню */
		public static const MAIN_MENU:String = "mainMenu";
		/** игровой экран */
		public static const GAME_SCREEN:String = "gameScreen";
		/** экран с отображением результата игры */
		public static const SCORE_SCREEN:String = "scoreScreen";
		/** экран настроек */
		public static const SETTINGS_SCREEN:String = "settingsScreen";
	}
}

Немного расширим класс кнопки, чтобы у неё было два состояния «активировано/не активировано». Это нужно для экрана настроек. Кнопка будет работать там, как триггер. После изменений класс Button будет выглядеть так:
package elements {
	
	import com.greensock.TweenLite;
	import flash.display.Bitmap;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.TouchEvent;
	import flash.text.TextField;
	import tools.Bitmaps;
	import tools.Tools;
	
	/**
	 * Класс кнопки, которая используется в меню и в других местах.
	 * 
	 * @author illuzor
	 */
	
	public class Button extends Sprite {
		/** @private текст для отображения на кнопке */
		private var text:String;
		/** @private битмап для фона кнопки */
		private var buttonImage:Bitmap;
		/** @private активирована ли кнопка */
		private var activated:Boolean;
		/**
		 * Конструктор слушает добавление на сцену.
		 * Тут stage нам нужен на случай, если произойдёт тап по кнопке и перемещение пальца в сторону от кнопки
		 * 
		 * @param	text текст для отображения на кнопке
		 */
		public function Button(text:String) {
			this.text = text;
			addEventListener(Event.ADDED_TO_STAGE, addedToStage);
		}
		/**
		 * @private добавление на сцену.
		 * добавляем графику и текстовое поле кнопки.
		 * 
		 * @param	e событие добавления на сцену
		 */
		private function addedToStage(e:Event):void {
			removeEventListener(Event.ADDED_TO_STAGE, addedToStage);
			
			buttonImage = Bitmaps.buttonBitmap; // добавляем битмап
			buttonImage.smoothing = true;
			addChild(buttonImage);
			
			var textField:TextField = Tools.generateTextField(50, text); // генерируем текстовое поле...
			textField.x = (buttonImage.width - textField.width) / 2; // ... позиционируем его и добавляем в дисплейЛист
			textField.y = (buttonImage.height - textField.height) / 2;
			addChild(textField);
			
			this.addEventListener(TouchEvent.TOUCH_BEGIN, touchBegin); // прикосновение к кнопке
			addEventListener(Event.REMOVED_FROM_STAGE, removedFromStage); // слушатель удаления со stage
		}
		/**
		 * @private Анимируем по альфе на половину
		 * 
		 * @param	e событие прикосновения пальцем к кнопке
		 */
		private function touchBegin(e:TouchEvent):void {
			if (!activated) { // если кнопка не активирована
				TweenLite.to(buttonImage, .3, { alpha:.5 } );
				stage.addEventListener(TouchEvent.TOUCH_END, touchEnd); // убирание пальца от дисплея после прикосновения к кнопке
			}
		}
		/**
		 * @private возвращаем альфу к единице
		 * 
		 * @param	e событие убирания пальца
		 */
		private function touchEnd(e:TouchEvent):void {
			if (!activated) { // если кнопка не активирована
				TweenLite.to(buttonImage, .3, { alpha:1 } );
				stage.removeEventListener(TouchEvent.TOUCH_END, touchEnd);
			}
		}
		/**
		 * Активация кнопки. Снижаем прозрачность
		 */
		public function activate():void {
			TweenLite.killTweensOf(buttonImage);
			buttonImage.alpha = .3;
			activated = true;
		}
		/**
		 * Деактивация кнопки. Приравниваем прозрачность единице
		 */
		public function deactivate():void {
			buttonImage.alpha = 1;
			activated = false;
		}
		/**
		 * при удалении со stage убиваем более не нужные слушатели
		 * 
		 * @param	e событие удаления со сцены
		 */
		private function removedFromStage(e:Event):void {
			removeEventListener(Event.REMOVED_FROM_STAGE, removedFromStage);
			this.removeEventListener(TouchEvent.TOUCH_BEGIN, touchBegin);
			stage.removeEventListener(TouchEvent.TOUCH_END, touchEnd);
		}
		
	}
}

Нужен экран настроек для выбора типа управления:



Создадим в пакете screens класс SettingsScreen
package screens {
	
	import constants.ControlType;
	import elements.Button;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.TouchEvent;
	import flash.sensors.Accelerometer;
	import flash.text.TextField;
	import tools.StorageManager;
	import tools.Tools;
	
	/**
	 * Экран настроек. Позволяет выбрать тип управления
	 * 
	 * @author illuzor
	 */
	
	public class SettingsScreen extends Sprite {
		/** @private кнопка-триггер для активации управления пальцем */
		private var touchButton:Button;
		/** @private кнопка-триггер для активации управления наклоном устройства */
		private var sensorButton:Button;
		/** кнопка save для выхода из экрана настроек и возвращения в главное меню */
		public var saveButton:Button;
		/**
		 * Слушаем добавление на сцену
		 */
		public function SettingsScreen() {
			addEventListener(Event.ADDED_TO_STAGE, addedToStage);
		}
		/**
		 * @private Создаём графические элементы и кнопку выхода в меню
		 * 
		 * @param	e событие добавления на сцену
		 */
		private function addedToStage(e:Event):void {
			removeEventListener(Event.ADDED_TO_STAGE, addedToStage);
			// текстовое поле для отображения текста "Control Type:"
			var controlText:TextField = Tools.generateTextField(30, "Control Type:");
			controlText.x = (stage.stageWidth - controlText.width) / 2;
			controlText.y = (stage.stageHeight - controlText.height) / 3;
			addChild(controlText);
			
			// далее создаём две кнопки
			touchButton = new Button("TOUCH");
			addChild(touchButton);
			touchButton.activate();
			touchButton.width = stage.stageWidth / 3;
			touchButton.scaleY = touchButton.scaleX;
			touchButton.x = stage.stageWidth / 2 - touchButton.width - 10;
			touchButton.y = controlText.y + controlText.height + 20;
			
			if (Accelerometer.isSupported) { // кнопку "SENSOR" создаём только если в устройстве есть акселерометр
				sensorButton = new Button("SENSOR");
				addChild(sensorButton);
				sensorButton.width = stage.stageWidth / 3;
				sensorButton.scaleY = sensorButton.scaleX;
				sensorButton.x = stage.stageWidth / 2 + 10;
				sensorButton.y = controlText.y + controlText.height + 20;
				// если в настройках выставлен тип ControlType.SENSOR_CONTROL, активируем кнопку "SENSOR"
				if (Settings.controlType == ControlType.SENSOR_CONTROL) setSensor(null); 
				touchButton.addEventListener(TouchEvent.TOUCH_TAP, setTouch);
				sensorButton.addEventListener(TouchEvent.TOUCH_TAP, setSensor);
			} else { // если акселерометра в устройстве нет, просто центруем кнопку "TOUCH"
				touchButton.x = (stage.stageWidth - touchButton.width) / 2;
			}
			// создаём кнопку для выхода в главное меню
			saveButton = new Button("SAVE");
			addChild(saveButton);
			saveButton.width = stage.stageWidth / 2;
			saveButton.scaleY = saveButton.scaleX;
			saveButton.x = (stage.stageWidth - saveButton.width) / 2;
			saveButton.y = stage.stageHeight - saveButton.height - 20;
		}
		/**
		 * @private задаём значение переменной в настройках, активируем/деактивируем кнопки
		 * 
		 * @param	e событие прикосновения к кнопке "TOUCH"
		 */
		private function setTouch(e:TouchEvent):void {
			Settings.controlType = ControlType.TOUCH_CONTROL;
			StorageManager.controlType = ControlType.TOUCH_CONTROL;
			touchButton.activate();
			sensorButton.deactivate();
		}
		/**
		 * @private задаём значение переменной в настройках, активируем/деактивируем кнопки
		 * 
		 * @param	e событие прикосновения к кнопке "SENSOR"
		 */
		private function setSensor(e:TouchEvent):void {
			Settings.controlType = ControlType.SENSOR_CONTROL;
			StorageManager.controlType = ControlType.SENSOR_CONTROL;
			sensorButton.activate();
			touchButton.deactivate();
		}
		
	}
}

Добавляем в класс MainMenu кнопку для вызова экрана настроек. После доработки класс будет выглядеть так:
gist.github.com/4051110

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

Дорабатываем основной класс для возможности показа экрана настроек:
gist.github.com/4051134

Теперь к системе управления. Создаём в пакете tools класс GameControl. Это менеджер управления. В конструктор он получает ссылку на stage и ссылку на платформу. Затем в зависимости от типа управления из настроек активирует соответствующую систему управления. Их две — пальцевая и управления наклоном смартфона.
package tools {
	
	import com.greensock.TweenLite;
	import constants.ControlType;
	import elements.Platform;
	import flash.display.Stage;
	import flash.events.AccelerometerEvent;
	import flash.events.MouseEvent;
	import flash.sensors.Accelerometer;
	
	/**
	 * Менеджер управления.
	 * Определяет способ управления платформой.
	 * Есть два способа - движение пальцем по дисплею и управление наклоном устройства
	 * 
	 * @author illuzor
	 */
	
	public class GameControl {
		/** @private ссылка на stage */
		private var container:Stage;
		/** @private ссылка на платформу */
		private var platform:Platform;
		/** @private акселерометр */
		private var accelerometer:Accelerometer;
		/**
		 * Конструктор. Проверяет тип управления из настроек 
		 * и на основании его активирует соответствующий тип управления
		 * 
		 * @param	platform ссылка на платформу
		 * @param	container ссылка на stage
		 */
		public function GameControl(platform:Platform, container:Stage) {
			this.platform = platform;
			this.container = container;
			
			// проверяем тип управления и запускаем соответствующую функцию
			if (Settings.controlType == ControlType.TOUCH_CONTROL) {
				activateTouchControl();
			} else {
				activateAccelerometerControl();
			}
		}
		/**
		 * @private активация управления пальцем
		 */
		private function activateTouchControl():void {
			container.addEventListener(MouseEvent.MOUSE_MOVE, moveplatform); // событие движения пальца по экрану
			container.addEventListener(MouseEvent.MOUSE_UP, moveplatform); // событие убирания пальца с экрана
		}
		/**
		 * @private двигаем платформу в зависимости от положения пальца на дисплее.
		 * 
		 * @param	e событие движения пальца по экрану или его убирания с экрана
		 */
		private function moveplatform(e:MouseEvent):void {
			if(container.mouseY > 50)TweenLite.to(platform, .36, { x:container.mouseX - platform.width/2 } );
		}
		/**
		 * @private активация управления акселерометром
		 */
		private function activateAccelerometerControl():void {
			accelerometer = new Accelerometer(); // создаём акселерометр
			accelerometer.addEventListener(AccelerometerEvent.UPDATE, updateAccelerometer); // и слушаем его обновление
		}
		/**
		 * @private В зависимости от наклона устройства по оси .x двигаем платформу.
		 * Подробное описание функции в тексте урока
		 * 
		 * @param	e событие обновления акселерометра
		 */
		private function updateAccelerometer(e:AccelerometerEvent):void {
			var percent:uint = -e.accelerationX * 100 + 50; // процент наклона из диапазона 0.5 - -.05
			if (e.accelerationX < -.5) percent = 100;
			if (e.accelerationX > .5) percent = 0;
			// двигаем платформу в зависимости от процента наклона
			TweenLite.to(platform, .36, { x:percent / 100 * container.stageWidth - platform.width / 2 } );
		}
		/**
		 *  Очистка. Удаление слушателей в зависимости от текущего типа настроек.
		 */
		public function clear():void {
			if (Settings.controlType == ControlType.TOUCH_CONTROL) {
				container.removeEventListener(MouseEvent.MOUSE_MOVE, moveplatform);
				container.removeEventListener(MouseEvent.MOUSE_UP, moveplatform);
			} else {
				accelerometer.removeEventListener(AccelerometerEvent.UPDATE, updateAccelerometer);
				accelerometer = null;
			}
		}
		
	}
}


И последнее — модификация класса GameScreen. Нужно удалить оттуда всё, что связано с управлением: два слушателя и функцию.И подключить новый менеджер управления.
private var control:GameControl;
//...
control = new GameControl(platform, stage); //После создания платфомы

Также надо не забыть про очистку менеджера управления при удалении со сцены экземпляра класса GameScreen
control.clear(); // очишаем менеджер управления
control = null;

Весь код обновлённого класса GameScreen: gist.github.com/4051156

С управлением всё. Можно тестировать.

Локальное хранилище.


Сделаем сохранение настроек в файл. Чтобы при закрытии игры они не терялись. Файл будет называть gameSettings.xml. Сохранять в нём будем настройку типа управления и пять последних результатов(очков) игры.
Выглядит он примерно так:

<xml>
	<controlType>type</controlType>
	<scores>
		<score>12</score>
		<score>0</score>
		<score>27</score>
		<score>5</score>
		<score>182</score>
	</scores>
</xml>

В AIR есть специальные классы для работы с файловой системой. Первый — File, который по сути представляет собой путь к файлу. Второй — FileStream. Он позволяет читать и записывать файлы без участия пользователя. Файл будет лежать в директории File.applicationStorageDirectory. Это специальный путь, уникальный для каждого приложения.
При изменении настройки типа управления она будет записываться в файл. При проигрыше игры, то есть, когда появился результат игры, этот результат тоже будет записываться в файл. И появляется новый экран для отображения пяти последних результатов.

В пакете tools создадим класс StorageManager. Он управляет чтением и записью файла, хранит временные результаты
и может отдавать некоторые значения. В коде подробные комментарии.
package tools {
	
	import flash.filesystem.File;
	import flash.filesystem.FileMode;
	import flash.filesystem.FileStream;
	
	/**
	 * Класс менеджера локального хранилища.
	 * Считывает из локальной памяти устройства файл с настройками и записывает в него изменения.
	 * Если файл не существует (то есть это первый запуск приложения), то он создаётся и записывается.
	 * Также может отдавать некоторые данные.
	 * 
	 * @author illuzor
	 */
	
	public class StorageManager {
		/** @private файл для чтения/сохранения */
		static private var settingsFile:File;
		/** @private xml с данными (а именно с типом управления и последними результатами) */
		static private var settingXML:XML;
		/** @private массив с результатами игры ( с количеством набранных очков) */
		static private var scoresList:Vector.<uint> = new Vector.<uint>();
		/**
		 * Инициализация.
		 * Создаётся файл и проверяется на существование в локальной памяти устройства.
		 */
		public static function init():void {
			// задание пути к локальному файлу для хранения данных
			settingsFile = File.applicationStorageDirectory.resolvePath("gameSettings.xml");
			if (!settingsFile.exists) { // если файл не существует...
				settingXML = createXML(); // ...создаём начальный xml ...
				writeFile(); // ... и записываем его в файл.
			} else { // если файл существует
				settingXML = readFile(); // считываем его и на основе этого файла создаём xml
				createList(); // заполняем массив с результатами из считанного xml
			}
		}
		/**
		 * @private Создаём начальный xml
		 * Записываем тип управления из настроек.
		 * 
		 * @return начальный xml
		 */
		private static function createXML():XML {
			return new XML("<xml><controlType>"+Settings.controlType+"</controlType><scores></scores></xml>");
		}
		/**
		 * @private Считывание файла из постоянной памяти устройства.
		 * 
		 * @return xml на основе считанного файла
		 */
		private static function readFile():XML {
			var xml:XML; // временный xml
			var stream:FileStream = new FileStream(); // поток для считывания
			stream.open(settingsFile, FileMode.READ); // открываем поток на чтение
			xml = XML(stream.readUTFBytes(stream.bytesAvailable)); // создаём xml на основе считанного файла
			stream.close(); // закрываем поток
			return xml;
		}
		/**
		 * @private запись xml файла в локальное хранилище(память) устройства.
		 */
		private static function writeFile():void {
			var stream:FileStream = new FileStream(); // поток для записи
			stream.open(settingsFile, FileMode.WRITE); // открываем поток на запись
			stream.writeUTFBytes(settingXML.toXMLString()); // записываем файл
			stream.close(); // закрываем поток
		}
		/**
		 * Сеттер для задания типа управления извне (а именно из экрана настроек)
		 */
		public static function set controlType(value:String):void {
			settingXML.controlType.setChildren(value) // задаём значение в xml
			writeFile(); // и сразу записываем изменённый xml в файл
		}
		/**
		 * Геттер типа управления. Нужен в главном классе для задания настройки управления при запуске приложения
		 */
		public static function get controlType():String {
			return settingXML.controlType; // возвращает тип управления их xml
		}
		/**
		 * Сеттер для добавления нового результата
		 * Вызывается в классе ScoreScreen, то есть, когда игра проиграна.
		 */
		public static function set newScore(value:uint):void {
			scoresList.unshift(value); // добавляем полученный результат в начало массива.
			updateScores(); // запуск функции обновления списка результатов в xml
		}
		/**
		 * Геттер возвращает 5(или меньше) последних результатов в виде массива
		 * на основе xml с данными. Используется в LastScoresScreen для получения списка разельтатов
		 */
		public static function get lastScores():Vector.<uint> {
			var list:Vector.<uint> = new Vector.<uint>();
			for (var i:int = 0; i < settingXML.scores.children().length(); i++) {
				list.push(settingXML.scores.children()[i]);
			}
			return list;
		}
		/**
		 * @private заполнение массива с результатами.
		 * Используется при считываении файла данных.
		 */
		private static function createList():void {
			for (var i:int = 0; i < settingXML.scores.children().length(); i++) {
				scoresList.push(settingXML.scores.children()[i]);
			}
		}
		/**
		 * @private Обновление xml. Считываение первых пяти результатов из массива,
		 * запись их в xml и запись xml в файл
		 */
		private static function updateScores():void {
			var scoresTempXML:XML = new XML("<scores></scores>") // временный xml элемент для списка очков
			var counter:uint; // счётчик итераций
			if (scoresList.length <= 5) { // он не должен быть больше пяти
				counter = scoresList.length;
			} else {
				counter = 5;
			}
			for (var i:int = 0; i < counter; i++) { // добавление элементов в scoresTempXML
				scoresTempXML.appendChild(new XML("<score>" + scoresList[i] + "</score>"))
			}
			// применение изменений к основному xml (settingXML)
			settingXML.scores.setChildren(scoresTempXML.children());
			writeFile(); // и запись xml в файл.
		}
		
	}
}

Создаём класс экрана отображения результатов — LastScoresScreen. Он берёт из StorageManager список последних результатов и отображает их
package screens {
	
	import elements.Button;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.text.TextField;
	import tools.StorageManager;
	import tools.Tools;
	
	/**
	 * Отображение пяти последних результатов.
	 * Экран содержит заголовок, список результатов и кнопку выхода
	 * 
	 * @author illuzor
	 */
	
	public class LastScoresScreen extends Sprite {
		/** кнопка для возврата в меню */
		public var backButton:Button;
		/**
		 * Ждём добавления на сцену
		 */
		public function LastScoresScreen() {
			addEventListener(Event.ADDED_TO_STAGE, addedToStage);
		}
		/**
		 * Отображаем графические элементы
		 * 
		 * @param	e событие добавления на сцену
		 */
		private function addedToStage(e:Event):void {
			removeEventListener(Event.ADDED_TO_STAGE, addedToStage);
			// текстовое поле заголовка
			var headerText:TextField = Tools.generateTextField(40, "YOUR LAST SCORES:");
			headerText.x = (stage.stageWidth - headerText.width) / 2;
			headerText.y = stage.stageHeight / 4;
			addChild(headerText)
			// контейнер для текстовых полей результатов
			var scoresContainer:Sprite = new Sprite();
			addChild(scoresContainer);
			// массив с актуальными результатами.
			// берётся из StorageManager
			var scoresList:Vector.<uint> = StorageManager.lastScores;
			
			// на основании длины массива добавляем в контейнер текстовые поля с результатами
			for (var i:int = 0; i < scoresList.length ; i++) {
				var scoreText:TextField =  Tools.generateTextField(30, "SCORE #" + String(i + 1) + " — " + scoresList[i]);
				if (i != 0) scoreText.y = scoresContainer.height + 20;
				scoresContainer.addChild(scoreText);
			}
			// позиционируем контейнер с результатами
			scoresContainer.x = (stage.stageWidth - scoresContainer.width) / 2;
			scoresContainer.y = headerText.y + headerText.height + 40;
			
			backButton = new Button("BACK"); // кнопка для возвращения в главное меню
			addChild(backButton);
			backButton.width = stage.stageWidth / 2;
			backButton.scaleY = backButton.scaleX;
			backButton.x = (stage.stageWidth - backButton.width) / 2;
			backButton.y = stage.stageHeight - backButton.height - 20;
		}
		
	}
}

Добавляем в класс ScreenType новую константу:
/** экран отображения последних пяти результатов */
public static const LAST_SCORES_SCREEN:String = "lastScoresScreen";


Снова модифицируем классы Main и MainMenu для добавления новой кнопки и нового экрана. Оба на гисте:
Main: gist.github.com/4051200
MainMenu: gist.github.com/4051201

Обратите внимание на класс Main, а именно на строки в конструкторе:
StorageManager.init(); // запускаем инициализацию менеджера локального хранилища
Settings.controlType = StorageManager.controlType; // на основе данных из локального хранилища применяем тип управления
Это инициализация менеджера локального хранилища. И применение настройки управления из него.

Нужно немного дополнить обработчики кнопок экрана SettingScreen для передачи значений в StorageManager
private function setTouch(e:TouchEvent):void {
   Settings.controlType = ControlType.TOUCH_CONTROL;
   StorageManager.controlType = ControlType.TOUCH_CONTROL; // передача значения
   touchButton.activate();
   sensorButton.deactivate();
  }

  private function setSensor(e:TouchEvent):void {
   Settings.controlType = ControlType.SENSOR_CONTROL;
   StorageManager.controlType = ControlType.SENSOR_CONTROL; // передача значения
   sensorButton.activate();
   touchButton.deactivate();
  }

И последнее. Чуть дополняем конструктор класса ScoreScreen
public function ScoreScreen(score:uint) {
   this.score = score;
   StorageManager.newScore = score; // добавляем новый результат в менеджер локального хранилища
   addEventListener(Event.ADDED_TO_STAGE, addedToStage);
  }


Вот и вторая часть урока подошла к концу.

Исходник
Готовый apk

(Прямая ссылка на зазипованый apk)

И в качестве бонуса: немного обновлённый код и моя попытка написать то же самое на java.

Ещё немного информации. Режимы сборки для Android.


Существует три режима сборки, их можно увидеть при запуске PackageApp.bat:



1 Релизная сборка. Apk можно отправлять в Play Store
2 Дебаг сборка
3 Сборка со встраиванием среды выполнения AIR в приложения, вес apk увеличивается примерно на 8 мегабайт. Установка AIR на Android при таком типе сборки не требуется. Также можно отправлять в Play Store.

Для загрузки приложения в Google Play Store нужно создать собственный сертификат в формате .p12 и подписать им приложение. Все следующие версии приложения обязательно подписывать тем же самым сертификатом. Создать его можно во Flash Builder или Flash Professional.

В файле bat/SetupApplication.bat нужно прописать имя сертификата, путь к нему и пароль:
:: Android packaging
set AND_CERT_NAME="AndroinSimpleGame"
set AND_CERT_PASS=fd
set AND_CERT_FILE=cert\AndroinSimpleGame01.p12


Сборка под ios и запуск.


Без аккаунта разработчика «по честному» не получится даже запаковать приложение для ios. Для сборки требуется сертификат разработчика, сертификат распространения (оба в формате .p12), пароли от сертификатов и два файла mobileprovision: один для тестирования, другой для релизной сборки. Достать это всё можно в дев центре apple при наличии аккаунта разработчика.
Прописывается всё так же в файле bat/SetupApplication.bat:
:: iOS packaging
set IOS_DIST_CERT_FILE=
set IOS_DEV_CERT_FILE=
set IOS_DEV_CERT_PASS=
set IOS_PROVISION=

При запуске PackageApp.bat мы видим 6 режимов сборки(см. скриншот выше):

[4] fast test (ipa-test-interpreter) тестовая сборка (не дебаговая).
[5] fast debug (ipa-debug-interpreter) дебаг сборка.

Чем отличается fast test от slow test и fast debug от slow debug, за исключением времени компиляции, я так и не понял.

[8] «ad-hoc» (ipa-ad-hoc) сборка для распространения среди тестировщиков.
[9] App Store (ipa-app-store) релизная сборка для распространения через AppStore.

После упаковки ipa файла, например в режиме fast test, нужно открыть Itunes раздел Приложения, перетащить туда файл mobileprovision и ipa файлы, затем правый клик по подключенному устройству и нажать синхронизировать. Через минуту приложение появится на устройстве. Можно тестировать.
Для ios среда выполнения AIR в любом случае встраивается в приложение.

UPDATE:
Во FlashDevelop версии 4.2.1 появилась автоустановка ios приложений. В файле Run.bat для :target нужно выставить значение goto ios-debug или goto ios-test. Теперь нужно просто нажать кнопку run(или F5). Приложение автоматически запакуется и установится на ios девайс. Всё, что остаётся сделать вручную — запустить приложение. Этот метод в разы удобней ручной сборки, заливки, синхронизации.
Важное замечание: в проектах, созданных в более ранних версиях FlashDevelop, автоустановка работать не будет. Нужно создать новый проект и перенести в него все классы, библиотеки и настройки.

Другие IDE.


Возможно, кому-то работа с мобильным AIR во FlashDevelop покажется сильно неудобной. На деле, стоит один раз разобраться и никаких трудностей не возникнет. Разработчики FlashDevelop уже давно планируют сделать работу с AIR проще, но как видно, за последний год с момента релиза 4.0 прогресса в этом деле почти никакого.
Существуют другие шикарные IDE, например Flash Builder и IDEA — в них работа с AIR намного проще и удобней.

Вот теперь действительно конец. Спасибо за внимание.
  • +10

Комментарии (13)

+1
Есть ещё урок по написанию native extention под андроид. Кому-нибудь интересно?
0
Конечно интересно!
0
а можно то же самое, но на Haxe? <смайл наивной улыбки>
0
С haxe я знаком очень поверхностно, так что ничего по этому поводу сказать не могу.
0
Для сборки требуется сертификат разработчика, сертификат распространения (оба в формате .p12)
Не очень понял это место. Сертификат нужен один в формате p12. А его создают из сертификата в формате cer полученного от Apple.
0
Я с ios имел дело всего пару раз, собственного аккаунта не имею. Просил у заказчика два сертификата, и он мне их без вопросов присылал.
0
А, понял. Да, действительно, нужны отдельные сертификаты для сборки дебажной версии и релизной (для ad hoc ограниченного распространения и он же для App Store). Тогда нужно указать, что mobileprovision таких же двух типов нужны.
0
Спасибо за дополнение. Добавил
0
Очень хорошая серия уроков. Продолжай, не останавливайся.
0
Спасибо.
Это не серия уроков, а один урок в двух частях.
+1
Нативный код под андроид пишется на С/С++, а на яве пишется код под дроидовскую JVM.
0
Меня вот какой вопрос всегда интересовал.
Как сделать так, чтобы после обновления игры (полная перекачка из маркета) оставался нетронутым файл настроек и прогресса?
0
Если новая версия ставится поверх старой, то всё сохранится.
Если хочется сохранить настройки даже после удаления, можно хранить их на сервере с привязкой к игровому аккаунту.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.