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

Вместо вступления.


Приветствую всех.
Этот урок был написан в марте 2012. Сейчас я решил опубликовать его на данном ресурсе. Код решил не менять, прошу сильно к нему не придираться(сам бы придрался). Немного доработан сам текст урока и комментарии в коде. Цель урока — не научить делать игры, что в рамках одного урока само по себе невозможно, а продемонстрировать основы работы с мобильным AIR. Почему именно Android, а не ios? Потому что на момент написания урока под рукой был только он. Под ios всё делается практически так же, но есть некоторые отличия, о которых написано в конце второй части урока.
Буду рад любым комментариям, замечаниям, указанием на ошибки.

Немного теории.


Adobe AIR позволяет создавать на ActionScript 3 и MXML для iOS и Android приложения, которые для пользователя ничем не будут отличать от нативных. Их, как и любые нативные приложения, можно распространять через фирменные магазины приложений Apple AppStore и Google Play Store (бывший Android Market). С версии AIR 3.2 появилась поддержка Stage3D. Для работы AIR приложения на Android нужно установить на устройство AIR Runtime, или же при компиляции в captive-runtime среда выполнения вшивается в apk. При этом установка AIR Runtime на девайс не требуется.

При работе с мобильными устройствами стоит учитывать, что разрешения их дисплеев ниже(уже есть и такие, у которых выше), чем у мониторов компьютеров и их физические размеры тоже значительно меньше. Также есть такие понятия, как «физический размер пикселя» и «плотность пикселей», поэтому нужно уделить внимание размеру различных графических элементов(кнопок, персонажей игры и т.д.). В общем, это целая наука и мы не будем останавливаться на ней подробно.
Метод ввода — сенсорный дисплей. Для обработки сенсорного ввода существует специальное событие TouchEvent, хотя и события мыши обрабатываются корректно. Также есть другие особенности, о которых я расскажу в ходе урока.
Мы будем делать очень простую игру для Android. Запускать её можно будет на смартфонах, планшетах и любых других устройствах.
Для работы среды выполнения AIR есть некоторые аппаратные и программные требования.
Для Android они выглядят следующим образом:
— Android версии 2.2 или выше;
— Процессор с архитектурой ARM7 с частотой минимум 550MHz;
— минимум 256 мегабайт оперативной памяти.

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

Для выполнения урока нам понадобится следующее:
FlashDevelop 4.2;
Flex SDK 4.6;
AIR SDK 3.5;
— библиотека от greensock;
— устройство на Android. Можно обойтись и эмулятором, но это не так интересно;
— установленный на устройство AIR Mobile.

Версии указаны актуальные на момент публикации урока. Уже есть Flex SDK 4.8, но по сути это тот же самый 4.6. И с 4.8 у меня FlashDevelop начинает непомерно поглощать оперативку, непонятно почему. Лучше всегда использовать последнюю версию AIR SDK, так как с каждым релизом добавляются новые возможности и исправляются ошибки. AIR SDK нужно просто распаковать в папку Flex SDK с заменой файлов и подключить Flex SDK к FlashDevelop.

Что именно будем делать.


Делать мы будем небольшую игру. Она представляем из себя следующее: Сверху экрана падают фигуры. Снизу находится платформа, которой надо эти фигуры ловить или не ловить в зависимости от их вида. Игра состоит из трёх экранов:
— главное меню с фоном, логотипом и двумя кнопками. Одна для начала игры, другая для выхода из приложения;
— игровой экран. Содержит платформу, падающие фигуры а также индикаторы текущего уровня и набранных очков. И кнопка выхода в главное меню;
— экран отображения количества набранных очков с кнопками для выхода в меню и «сыграть ещё раз».

Пара скриншотов:


Приступаем.


Для начала, убедитесь, что на устройство установлен AIR, если не установлен, установите. Также установите на компьютер драйвера для вашего устройства для подключения к с помощью кабеля.

Запускаем FlashDevelop и создаём новый проект AIR AS3 Mobile app. Вот так выглядит созданный проект:



Что мы перед собой видим:

Папка bat содержит несколько пакетных файлов:
CreateCertificate.bat нужен для генерации self-signed сертификата для Android. Без этого сертификата не получится собрать apk;
InstallAirRuntime.bat устанавливает AIR runtime на Android устройство из %FLEX_SDK%\runtimes\air\android\device\runtime.apk. Лучше самостоятельно установить последнюю версию из Android Play Store;
Packager.bat упаковывает флешку в apk(android) или ipa(ios);
SetupApplication.bat содержит различные параметры приложения (пути к сертификатам, пароли от них, название приложения и т.п.);
SetupSDK.bat содержит путь к FlexSDK. Определяется автоматически, если не определился, нужно прописать вручную. И также содержит путь к AndroidSDK. На самом деле из него нужны только три файла: adb.exe, AdbWinApi.dll и AdbWinUsbApi.dll. Причём они уже содержатся в дистрибутиве FlashDevelop и путь к ним прописывается также автоматически.

В папку bin помещается скомпилированный swf файл.
В папке cert должны лежать сертификаты.
Папка icons содержит наборы иконок.
Две стандартные папки lib и src для библиотек и классов соответственно.

Корневая папка:
— Стандартный для AIR приложений application.xml с различными параметрами приложения;
PackageApp.bat позволяет выбрать платформу и тип упаковки приложения. После выбора упаковывает. Появляется папка dist c apk или ipa;
Run.bat заливает приложение на мобильное устройство и запускает его там;
— Два файла AIR_Android_readme.txt и AIR_iOS_readme.txt с инструкциями.

Настройка проекта.


Открываем настройки проекта Project/Propeties, меняем цвет фона на чёрный. Разрешение значения не имеет. Будем подстраиваться под текущее.
Версию AIR выставляем 3.5. В итоге параметры должны выглядеть так:



Открываем файл application.xml, в нём во второй строке меняем версию AIR на 3.5:
<application xmlns="http://ns.adobe.com/air/application/3.5">

Открываем настройки AIR — Project/AIR App Properties. В открывшемся окне переходим к вкладке Initial window. В этом окне выбираем Non-Windowed Platforms.
Переключаем режим отображения на портретный. И выключаем Auto Orient, так как игра будет только в портретном режиме. Render Mode выставляем CPU или Direct. Настройки должны выглядеть вот так:



Далее прям во FlashDevelop двойным щелчком открываем файл Run.bat и меняем строку goto desktop на goto android-debug. Это нужно для тестирования проекта на устройстве. Если оставить goto desktop, проект будет запускаться на эмуляторе.

Теперь нам нужно сгенерировать сертификат. Без него не получится запаковать приложение в .apk. Если папка cert отсутствует в проекте, создайте её вручную. Это важно, так как bat файл не сможет достучаться до несуществующей папки. Запускаем файл bat/CreateCertificate.bat(правый клик/Execute), в папке cert появляется наш сертификат со стандартным паролем «fd». Стоит сказать, что любое приложение для Android должно быть подписано сертификатом. То, что мы сгенерировали, это так называемый «сертификат для разработки». Его нам на данном этапе достаточно.

Из архива с сайта greensock.com достаём файл greensock.swc, кладём его в папку lib и подключаем к проекту (правый клик/Add To Library).

С настройкой всё. Добавим к проекту нужную графику и иконки. Создайте папку assets и положите туда графику из архива, также замените иконки. Или же можете нарисовать собственные аналоги.

Приступаем к коду.


А теперь самое главное и интересное — пишем код. Классы игры выглядят следующим образом:



Далее идёт код классов. К каждому есть пояснение и комментарии в коде.
Начнём с констант (пакет constants) и событий (пакет events)

ItemType — типы предметов. Тех самых, которые падают сверху экрана
package constants {
	
	/**
	 * Статические константы для определния типа игрового объекта.
	 * 
	 * @author illuzor
	 */
	
	public class ItemType {
		/** обычный объект. Прибавляет единицу к очкам */
		public static const GOOD:String = "goodItem";
		/** "очень хороший" объект. Прибавляет 5 к очкам */
		public static const VERY_GOOD:String = "veryGoodItem";
		/** "злой" объект. Отнимает единицу от очков */
		public static const EVIL:String = "evilitem";
		
	}
}

ScreenType — типы экранов. Всего у нас их три, и описаны они выше
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";
		
	}
}

GameEvent — игровые события
package events {
	
	import flash.events.Event;
	
	/**
	 * Игровые события.
	 * 
	 * @author illuzor
	 */
	
	public class GameEvent extends Event {
		/** выход из игры в главное меню через кнопку menu */
		public static const EXIT_GAME:String = "exitGame";
		/** игра проиграна */
		public static const GAME_OVER:String = "gameOver";
		
		public function GameEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false) { 
			super(type, bubbles, cancelable);
		} 
		
		public override function clone():Event { 
			return new GameEvent(type, bubbles, cancelable);
		} 
		
		public override function toString():String { 
			return formatToString("GameEvent", "type", "bubbles", "cancelable", "eventPhase"); 
		}
		
	}
}

Теперь рассмотрим пакет elements. Это различные графические элементы, которые используются в игре.

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;
		/**
		 * Конструктор слушает добавления на сцену.
		 * тут 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
		}
		/**
		 * анимируем по альфе на половину
		 * 
		 * @param	e событие прикосновения пальцем к кнопке
		 */
		private function touchBegin(e:TouchEvent):void {
			TweenLite.to(buttonImage, .3, { alpha:.5 } );
			stage.addEventListener(TouchEvent.TOUCH_END, touchEnd); // убирание пальца от дисплея после прикосновения к кнопке
		}
		/**
		 * возвращаем альфу к единице
		 * 
		 * @param	e событие убирания пальца
		 */
		private function touchEnd(e:TouchEvent):void {
			TweenLite.to(buttonImage, .3, { alpha:1 } );
			stage.removeEventListener(TouchEvent.TOUCH_END, touchEnd)
		}
		/**
		 * при удалении со 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)
		}
		
	}
}

Item — предмет, который падает сверху в процессе игры. Может быть трёх типов, они описаны в коде
package elements {
	
	import constants.ItemType;
	import flash.display.Shape;
	import flash.filters.GlowFilter;
	
	/**
	 * Класс айтема (предмета), который падаёт сверху экрана.
	 * 
	 * @author illuzor
	 */
	
	public class Item extends Shape {
		/** тип айтема из constants.ItemType */
		public var type:String;
		/** скорость движения айтема */
		public var speed:uint;
		/**
		 * В конструкторе рисуется графика айтема.
		 * 
		 * @param	type тип айтема
		 */
		public function Item(type:String) {
			this.type = type;
			
			switch (type) { // проверяем, какой тип передан в конструктор
				// и в зависимости от этого рисуем соответствующую графику.
				case ItemType.GOOD: // рисуем зелёный квадрат
					this.graphics.beginFill(0x00A400);
					this.graphics.drawRect(0, 0, 14, 14);
					this.graphics.endFill();
				break;
				
				case ItemType.VERY_GOOD: // рисуем синий квадрат со свечением
					this.graphics.beginFill(0x01A6FE);
					this.graphics.drawRect(0, 0, 14, 14);
					this.graphics.endFill();
					this.filters = [new GlowFilter(0x00FF00, 1, 4, 4, 4, 2)];
				break;
				
				case ItemType.EVIL: // рисуем красный круг
					graphics.beginFill(0xFF0000);
					graphics.drawCircle(0, 0, 7);
					graphics.endFill();
				break;
			}
		}
		
	}
}

Platform — «главный герой» игры. Небольшая платформа, которой управляет игрок.
package elements {
	
	import flash.display.Shape;
	import flash.filters.GlowFilter;
	
	/**
	 * Платформа для ловли объектов, которая находится в нижней части экрана.
	 * Тут всё очень просто, рисуется белый прямоугольник и применяется фильтр свечения.
	 * Отдельный класс для того, чтобы платформа воспринималась, как отдельная игровая единица.
	 * 
	 * @author illuzor
	 */
	
	public class Platform extends Shape {
		
		public function Platform() {
			this.graphics.clear();
			this.graphics.beginFill(0xFFFFFF);
			this.graphics.drawRect(0, 0, 110, 24);
			this.graphics.endFill();
			this.filters = [new GlowFilter(0x00FF00, 3, 3)];
		}
		
	}
}

Пакет tools с инструментами.

Класс Bitmaps содержит прикреплённую графику и методы её получения извне
package tools {
	
	import flash.display.Bitmap;
	
	/**
	 * "Генератор" битмапов из прикреплённых файлов
	 * 
	 * @author illuzor
	 */
	
	public class Bitmaps {
		/** @private прикреплённый файл графики фона */
		[Embed(source = "../../assets/MenuBackground.jpg")] private static var BackgroundBitmap:Class;
		/** @private прикреплённый файл графики для кнопки */
		[Embed(source = "../../assets/ButtonImage.png")] private static var ButtonBitmap:Class;
		/** @private прикреплённый файл графики для логотипа */
		[Embed(source = "../../assets/GameLogo.png")] private static var LogoBitmap:Class;
		
		/** Битмап фона */
		public static function get backgroundBitmap():Bitmap {
			return new BackgroundBitmap() as Bitmap;
		}
		/**  Битмап кнопки  */
		public static function get buttonBitmap():Bitmap {
			return new ButtonBitmap() as Bitmap;
		}
		/** Битмап логотипа  */
		public static function get logoBitmap():Bitmap {
			return new LogoBitmap() as Bitmap;
		}
		
	}
}

Класс Tools содержит другие инструменты. Пока что, только генератор текстфилда
package tools {
	
	import flash.text.TextField;
	import flash.text.TextFormat;
	
	/**
	 * Класс с небольшими инструментами. 
	 * Пока что содержит только генератор текстовового поля.
	 * 
	 * @author illuzor
	 */
	
	public class Tools {
		/**
		 * Генератор текстфилда по заданным параметрам
		 * 
		 * @param	size размер шрифта
		 * @param	text текст для отображения
		 * @param	color цвет текста
		 * @return настроенное текстовое поле
		 */
		public static function generateTextField(size:uint, text:String = "", color:uint = 0xFFFFFF):TextField {
			var textFormat:TextFormat = new TextFormat();
			textFormat.color = color;
			textFormat.size = size;
			
			var textField:TextField = new TextField();
			textField.selectable = false;
			textField.defaultTextFormat = textFormat;
			textField.text = text;
			textField.width = textField.textWidth +4;
			textField.height = textField.textHeight +4;
			
			return textField;
		}
		
	}
}

Экраны приложения из пакета screens

MainMenu — главное меню игры. Отображается сразу после запуска. Содержит кнопки, фон и логотип.
package screens {
	
	import flash.display.Bitmap;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.display.DisplayObject;
	import tools.Bitmaps;
	import elements.Button;
	
	/**
	 * Класс главного меню игры.
	 * Тут отображется фон, логотип и две кнопки.
	 *
	 * @author illuzor
	 */
	public class MainMenu extends Sprite {
		/** кнопка "PLAY" */
		public var playButton:Button;
		/** кнопка "EXIT" */
		public var exitButton:Button;
		/**
		 * В конструкторе просто слушаем добавление на сцену
		 */
		public function MainMenu() {
			addEventListener(Event.ADDED_TO_STAGE, adddedToStage);
		}
		/**
		 * создаём и добавляем фон, логотип, кнопки
		 * 
		 * @param	e событие добавления на сцену
		 */
		private function adddedToStage(e:Event):void {
			removeEventListener(Event.ADDED_TO_STAGE, adddedToStage);
			
			// создаём битмап фона и добавляем его на сцену
			var background:Bitmap = Bitmaps.backgroundBitmap;
			background.smoothing = true;
			addChild(background);
			placeBackground(background); // позиционируем фон
			
			// создаём битмап логотипа, задаём размер относительно ширины сцены
			// задаём положение и добавляем на сцену
			var logo:Bitmap = Bitmaps.logoBitmap;
			logo.smoothing = true;
			logo.width = stage.stageWidth * .7;
			logo.scaleY = logo.scaleX;
			logo.x = (stage.stageWidth - logo.width) / 2;
			logo.y = stage.stageHeight / 5;
			addChild(logo);
			
			// контейнер для кнопок для удобного позиционирования этих кнопок
			var buttonsContainer:Sprite = new Sprite();
			addChild(buttonsContainer);
			
			// создание кнопок "PLAY" и "EXIT", подгонка их размеров и добавление в контейнер
			playButton = new Button("PLAY");
			buttonsContainer.addChild(playButton);
			playButton.width = stage.stageWidth / 2;
			playButton.scaleY = playButton.scaleX;
			
			exitButton = new Button("EXIT");
			buttonsContainer.addChild(exitButton);
			exitButton.y = buttonsContainer.height + 25;
			exitButton.width = stage.stageWidth / 2;
			exitButton.scaleY = exitButton.scaleX;
			
			// позиционирование контейнера с кнопками
			buttonsContainer.x = (stage.stageWidth - buttonsContainer.width) / 2;
			buttonsContainer.y = (stage.stageHeight - buttonsContainer.height) / 2 + stage.stageWidth / 6;
		}
		/**
		 * Эта функция делает так, что переданный ей DisplayObject заполняет собой всю сцену
		 * без изменения пропорций. В нашем случае это фоновое изображение
		 * 
		 * @param	scaledObject DisplayObject для подгонки
		 */
		private function placeBackground(scaledObject:DisplayObject):void {
			scaledObject.scaleX = scaledObject.scaleY = 1;
			var scale:Number;
			if (scaledObject.width / scaledObject.height > stage.stageWidth / stage.stageHeight){
				scale = stage.stageHeight / scaledObject.height;
			}
			else {
				scale = stage.stageWidth / scaledObject.width;
			}
			scaledObject.scaleX = scaledObject.scaleY = scale;
			scaledObject.x = (stage.stageWidth - scaledObject.width) / 2;
			scaledObject.y = (stage.stageHeight - scaledObject.height) / 2;
		}
	
	}
}

В классе GameScreen проходит игровой процесс. Сверху падают айтемы. В зависимости от их типа нужно ловить их или избегать. Управляет игровым циклом
package screens {
	
	import com.greensock.TweenLite;
	import constants.ItemType;
	import elements.Button;
	import elements.Platform;
	import elements.Item;
	import events.GameEvent;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.events.TimerEvent;
	import flash.events.TouchEvent;
	import flash.text.TextField;
	import flash.utils.Timer;
	import tools.Tools;
	
	/**
	 * Основной класс игры. Что предствляет собой игровой процесс:
	 * Внизу экрана находится платформа, которую можно двигать влево/вправо движением пальца по экрану в любой части экрана.
	 * Сверху падают так называемые айтемы, которые могут быть трёх видов:
	 * - ItemType.GOOD - обычный айтем. при собирании прибавляет единицу к очками, при пропускании(уход за нижнюю границу экрана)
	 * отнимает единицу от очков
	 * - ItemType.VERY_GOOD - "усиленный" айтем. При собирании прибавляет 5 к очками. При пропускании ничего не происходит.
	 * - ItemType.EVIL - "злой" айтем. При собирании отнимает единицу от очков. При пропускании ничего не происходит.
	 * 
	 * При значении очков меньше пяти засчитывается проигрыш, а результатом игры считается максимальное набранное количество очков.
	 * С каждым новым уровнем айтемы движутся быстрей, чем в предыдущем.
	 * 
	 * Сверху слева находится идикатор набранных очков, справа сверху кнопка выхода в главное меню.
	 * Снизу в центре под платформой находится индикатор текущего уровня.
	 * 
	 * 
	 * @author illuzor
	 */
	
	[Event(name = "gameOver", type = "events.GameEvent")]
	[Event(name = "exitGame", type = "events.GameEvent")]
	
	public class GameScreen extends Sprite {
		/** @private контейнер для платформы и айтемов */
		private var gameContainer:Sprite;
		/** @private кнопка для выхода в главное меню */
		private var menuButton:Button;
		/** @private платформа */
		private var platform:Platform;
		/** @private игровой таймер. нужен для добавления нового айтема */
		private var gameTimer:Timer;
		/** @private номер текущего уровня */
		private var currentLevel:uint;
		/** @private текущее количество очков */
		private var currentScore:int;
		/** @private текстовое поле для отображения номера уровня */
		private var levelText:TextField;
		/** @private текстовое поле для отображения очков */
		private var scoreText:TextField;
		/** максимальное количество очков */
		public var maxScore:uint;
		/**
		 * В конструкторе слушаем добавление на сцену.
		 */
		public function GameScreen() {
			addEventListener(Event.ADDED_TO_STAGE, addedtToStage);
		}
		/**
		 * создаём элементы экрана
		 * 
		 * @param	e событие добавления на сцену
		 */
		private function addedtToStage(e:Event):void {
			removeEventListener(Event.ADDED_TO_STAGE, addedtToStage);
			
			gameContainer = new Sprite(); // контейнер
			addChild(gameContainer);
			
			platform = new Platform(); // создаём платформу, подгоняем её размер и положение и добавляем на сцену
			platform.width = stage.stageWidth * .18;
			platform.scaleY = platform.scaleX;
			platform.x = (stage.stageWidth - platform.width) / 2;
			platform.y = stage.stageHeight * .88;
			addChild(platform);
			
			scoreText = Tools.generateTextField(30, "SCORE: 0"); // текстовое поле очков
			scoreText.width = stage.stageWidth / 2;
			scoreText.x = scoreText.y = 10;
			addChild(scoreText);
			
			levelText = Tools.generateTextField(30, "LEVEL: 1"); // текстовое поле уровня
			levelText.width = levelText.textWidth + 4;
			levelText.x = (stage.stageWidth - levelText.width) / 2;
			levelText.y = stage.stageHeight - levelText.height - 20;
			addChild(levelText);
			
			menuButton = new Button("MENU");// кнопка для выхода в главное меню.
			addChild(menuButton);
			menuButton.width = stage.stageWidth / 3.2;
			menuButton.scaleY = menuButton.scaleX;
			menuButton.x = stage.stageWidth - menuButton.width - 10;
			menuButton.y = 10;
			
			startNewLevel(); // запускаем новый уровень
			
			menuButton.addEventListener(TouchEvent.TOUCH_TAP, exitGame); // событие нажатия на кнопку выхода в меню
			stage.addEventListener(MouseEvent.MOUSE_MOVE, moveplatform); // событие движения пальца по экрану
			stage.addEventListener(MouseEvent.MOUSE_UP, moveplatform); // событие убирания пальца с экрана
			addEventListener(Event.ENTER_FRAME, updateGame); // обновление состояния игры
			addEventListener(Event.REMOVED_FROM_STAGE, removedFromStage);
		}
		/**
		 * @private двигаем платформу в зависимости от положения пальца на дисплее.
		 * 
		 * @param	e событие движения пальца по экрану или его убирания с экрана
		 */
		private function moveplatform(e:MouseEvent):void {
			if(mouseY > menuButton.y + menuButton.height)TweenLite.to(platform, .36, { x:mouseX + -platform.width/2 } );
		}
		/**
		 * @private запуск нового уровня. каждый уровень состоит из 20 айтемов
		 */
		private function startNewLevel():void {
			var interval:uint = 2300; // интервал вызова таймера
			if (2300 - currentLevel * 350 < 250) { // чем выше уровень, тем меньше интервал
				interval = 350;
			} else {
				interval = 2300 - currentLevel * 350;
			}
			gameTimer = new Timer(interval, 20); // создаём и запускаем таймер.
			gameTimer.start();
			gameTimer.addEventListener(TimerEvent.TIMER, addItem);
			gameTimer.addEventListener(TimerEvent.TIMER_COMPLETE, cicleEnd);
		}
		/**
		 * @private Создаём новый айтем по таймеру
		 * 
		 * @param	e событие тика таймера
		 */
		private function addItem(e:TimerEvent):void {
			var randomRange:Number = Math.random(); // случайное значене
			var itemType:String = ItemType.GOOD; // тип нового айтема. по умолчнанию все айтемы обычные
			if (randomRange > 0.65 && randomRange < .95) { // если случайное значение в заданном диапазоне (30%)...
				itemType = ItemType.EVIL; // айтем злой
			} else if (randomRange >= .95 ){ // 5% айтемов пусть будут "усиленными"
				itemType = ItemType.VERY_GOOD;
			}
			var item:Item = new Item(itemType); // создаём новый айтем со сгенерированным типом
			item.x = stage.stageWidth * Math.random(); // помещаем на случайны .x
			item.y = -item.height; // а по .y убираем за пределы сцены
			item.speed = currentLevel+1; // скорость айтема
			gameContainer.addChild(item); // и добавлеем его на сцену
		}
		/**
		 * @private когда таймер закончил работу, очищаем, что не нужно и запускаем новый уровень
		 * 
		 * @param	e событие окончания работы таймера
		 */
		private function cicleEnd(e:TimerEvent):void {
			gameTimer.removeEventListener(TimerEvent.TIMER, addItem);
			gameTimer.removeEventListener(TimerEvent.TIMER_COMPLETE, cicleEnd);
			gameTimer = null; // удаляем слушаели с таймера и ссылку на таймер
			currentLevel++; // увеличиваем уровень на единицу
			levelText.text = "LEVEL: " + String(currentLevel + 1); // обновляем текст уровня
			levelText.width = levelText.textWidth +4;
			startNewLevel();// запускаем новый уровень
		}
		/**
		 * @private Игровой цикл
		 * Обновляем положения айтемов. если они ушли за пределы сцены, удаляем.
		 * Обратываем их столкновения с платформой
		 * 
		 * @param	e enterFrame событие
		 */
		private function updateGame(e:Event):void {
			// в цикле проходимся по всему содержимому игрового контейнера, кроме платформы (i = 1)
			for (var i:int = 1; i < gameContainer.numChildren; i++) { 
				var tempItem:Item = gameContainer.getChildAt(i) as Item; // берём айтем
				tempItem.y += (tempItem.speed * 3) * .8; // увеличиваем его .y координаты в зависимости от его скорости.
				if (tempItem.y > stage.stageHeight) { // если он ушёл вниз за сцену ...
					gameContainer.removeChild(tempItem); //... удаляем его ...
					if (tempItem.type == ItemType.GOOD) currentScore--; // .. а если его тип при этом оказался обычным, отнимаем единицу от очков.
				}
				if (tempItem.hitTestObject(platform)) { // если айтем пойман платформой
					// в зависимости от его типа, производим действие.
					// думаю, тут ничего не надо объяснять
					switch (tempItem.type) {
						case ItemType.GOOD:
							currentScore++;
						break;
						
						case ItemType.VERY_GOOD:
							currentScore +=5;
						break;
						
						case ItemType.EVIL:
							currentScore--;
						break;
					}
					// также при попадании на платформу айтем больше не нужен, удаляем его со сцены
					gameContainer.removeChild(tempItem);
				}
			}
			scoreText.text = "SCORE: " + currentScore; // обновляем тексовое поле с очками
			if (maxScore < currentScore) maxScore = currentScore; // записываем максимальное количество очков
			if (currentScore < -5) { // если количество очков меньше, чем -5..
				dispatchEvent(new GameEvent(GameEvent.GAME_OVER)); //...  генерируем событие проигрыша
			}
		}
		/**
		 * @private генерируем событие выхода из игры
		 * 
		 * @param	e событие прикосновения к кнопке выхода в меню
		 */
		private function exitGame(e:TouchEvent):void {
			dispatchEvent(new GameEvent(GameEvent.EXIT_GAME));
		}
		/**
		 * @private удаляем все ненужные больше слушатели
		 * 
		 * @param	e событие удаления со сцены
		 */
		private function removedFromStage(e:Event):void {
			removeEventListener(Event.REMOVED_FROM_STAGE, removedFromStage);
			removeEventListener(Event.ENTER_FRAME, updateGame);
			stage.removeEventListener(MouseEvent.MOUSE_MOVE, moveplatform);
			stage.removeEventListener(MouseEvent.MOUSE_UP, moveplatform);
			menuButton.removeEventListener(TouchEvent.TOUCH_TAP, exitGame);
			gameTimer.stop();
			gameTimer.removeEventListener(TimerEvent.TIMER, addItem);
			gameTimer.removeEventListener(TimerEvent.TIMER_COMPLETE, cicleEnd);
		}
		
	}
}

ScoreScreen отображается после проигрыша. Показывает текстовое поле с итоговым результатом и кнопки для возврата в меню и повторения игры
package screens {
	
	import elements.Button;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.text.TextField;
	import tools.Tools;
	
	/**
	 * Экран для отображения результата игры в виде количества набранных очков.
	 * Состоит из текста "YOUR SCORE: " и кнопок "MENU" и "AGAIN"
	 * 
	 * @author illuzor
	 */
	
	public class ScoreScreen extends Sprite {
		/** @private количество очков для отображения */
		private var score:uint;
		/** кнопка  "MENU" */
		public var menuButton:Button;
		/** кнопка  "AGAIN" */
		public var againButton:Button;
		/**
		 * В конструкторе ждём добавления на stage. Тут stage нужен для позиционирования элементов
		 * @param	score количество очков для отображения
		 */
		public function ScoreScreen(score:uint) {
			this.score = score;
			addEventListener(Event.ADDED_TO_STAGE, addedToStage);
		}
		/**
		 * создаём текстовое поле для отображения очков и кнопки для повтора игры и возврата в главное меню.
		 * 
		 * @param	e событие добавления на сцену
		 */
		private function addedToStage(e:Event):void {
			removeEventListener(Event.ADDED_TO_STAGE, addedToStage);
			
			// текстовое поле отображет количество набранных очков
			var scoreText:TextField = Tools.generateTextField(40, "YOUR SCORE: " + score );
			scoreText.x = (stage.stageWidth - scoreText.width) / 2;
			scoreText.y = (stage.stageHeight - scoreText.height) / 2 - stage.stageHeight / 6;
			addChild(scoreText);
			
			// кнопка для выхода в главное меню
			menuButton = new Button("MENU");
			addChild(menuButton);
			menuButton.width = stage.stageWidth / 2;
			menuButton.scaleY = menuButton.scaleX;
			menuButton.x = (stage.stageWidth - menuButton.width) / 2;
			menuButton.y = scoreText.y + scoreText.height + 30;
			
			// кнопка "сыграть ещё"
			againButton = new Button("AGAIN");
			addChild(againButton);
			againButton.width = stage.stageWidth / 2;
			againButton.scaleY = againButton.scaleX;
			againButton.x = (stage.stageWidth - againButton.width) / 2;
			againButton.y = menuButton.y + menuButton.height + 30;
		}
		
	}
}

И последнее — класс Main из корня. Основной класс игры. Служит для переключения экранов и очистки при этих переключениях. Слушает кнопки экранов на нажатие и игровые события. В зависимости от них очищает сцену и показывает нужный экран
package {
	
	import constants.ScreenType;
	import events.GameEvent;
	import flash.desktop.NativeApplication;
	import flash.display.Sprite;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.events.Event;
	import flash.events.TouchEvent;
	import flash.ui.Multitouch;
	import flash.ui.MultitouchInputMode;
	import screens.GameScreen;
	import screens.MainMenu;
	import screens.ScoreScreen;
	
	/**
	 * Основной класс игры. Управляет отображением разных экранов игры.
	 * Игра представляет из себя следующее: внизу экрана находится "платформа", которую можно передвигать.
	 * Сверху падают "предметы" трёх типов: красный, зелёный, синий. Нужно ловить их платформой.
	 * 
	 * Создано с помощью FlashDevelop 4.0.1 и Flex SDK 4.6
	 * С использованием библиотеки от greensock - http://www.greensock.com/v11/
	 * 
	 * @author illuzor
	 * @version 0.6
	 */
	
	public class Main extends Sprite {
		/** @private экран главного меню */
		private var menuScreen:MainMenu;
		/** @private экран игры */
		private var gameScreen:GameScreen;
		/** @private экран отображения результата игры (набарнных очков) */
		private var scoreScreen:ScoreScreen;
		/** @private эта переменная хранит текстовое значения типа экрана 
		 * из constants.ScreenType, который отображается в данный момент 
		 * нужна для корретной очистки от слушателей и экранных объектов */
		private var currentScreen:String;
		
		/**
		 * Главный конструктор
		 */
		public function Main():void {
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.align = StageAlign.TOP_LEFT;
			// событие деактивации приложения. то есть выхода из него или сворачивания (нажитии кнопки "домой" или "назад")
			stage.addEventListener(Event.DEACTIVATE, deactivate);
			// тип ввода. TOUCH_POINT выставлен по умолчанию и нам он подходит
			Multitouch.inputMode = MultitouchInputMode.TOUCH_POINT;
			
			showMenu(); // показываем главное меню.
		}
		/**
		 * @private показываем главное меню
		 */
		private function showMenu():void {
			currentScreen = ScreenType.MAIN_MENU; // применяем тип экрана
			menuScreen = new MainMenu(); // создаём меню и добавляем его на сцену
			addChild(menuScreen);
			
			menuScreen.playButton.addEventListener(TouchEvent.TOUCH_TAP, startGame); // добавляем слушатели к кнопкам меню
			menuScreen.exitButton.addEventListener(TouchEvent.TOUCH_TAP, deactivate);
		}
		/**
		 * @private показываем игровой экран
		 * 
		 * @param	e событие прикосновения к кнопке playButton главного меню
		 */
		private function startGame(e:TouchEvent):void {
			clear(); // очищаем
			currentScreen = ScreenType.GAME_SCREEN; // применяем тип экрана
			
			gameScreen = new GameScreen(); // создаём игровой экран и добавляем на сцену
			addChild(gameScreen);
			gameScreen.addEventListener(GameEvent.EXIT_GAME, exitGame); // событие выхода из игры по кнопке
			gameScreen.addEventListener(GameEvent.GAME_OVER, gameOver); // событие проигрыша
		}
		/**
		 * @private выход из игры по нажатию кнопки
		 * 
		 * @param	e событие выхода из игры
		 */
		private function exitGame(e:GameEvent):void {
			clear(); // очищаем
			showMenu(); // показываем главное меню
		}
		/**
		 * @private игра проиграна, показ результата
		 * 
		 * @param	e событие проигрыша
		 */
		private function gameOver(e:GameEvent):void {
			var score:uint = gameScreen.maxScore; // количество очков. достаётся из игрового экрана из переменной maxScore
			clear(); // очищаем
			currentScreen = ScreenType.SCORE_SCREEN; // применяем тип экрана
			scoreScreen = new ScoreScreen(score); // создаём и показываем экран результатов
			addChild(scoreScreen);
			scoreScreen.menuButton.addEventListener(TouchEvent.TOUCH_TAP, exitScore); // слушатели кнопок экрана результатов
			scoreScreen.againButton.addEventListener(TouchEvent.TOUCH_TAP, startGame); 
		}
		/**
		 * @private выход из экрана результатов по кнопке.
		 * нужно сделать то же самое, что и при нажатии кнопки выхода из игры, поэтому просто вызываем exitGame()
		 * 
		 * @param	e событие прикосновения к кнопке выхода из экрана результатов
		 */
		private function exitScore(e:TouchEvent):void {
			exitGame(null);
		}
		/**
		 * @private очистка от ненужных слушателей и экранных объектов
		 * в зависимости от текущего экрана.
		 */
		private function clear():void {
			switch (currentScreen) {
				case ScreenType.MAIN_MENU:
					menuScreen.playButton.removeEventListener(TouchEvent.TOUCH_TAP, startGame);
					menuScreen.exitButton.removeEventListener(TouchEvent.TOUCH_TAP, deactivate);
					removeChild(menuScreen);
					menuScreen = null;
				break;
				
				case ScreenType.GAME_SCREEN:
					gameScreen.removeEventListener(GameEvent.EXIT_GAME, exitGame);
					gameScreen.removeEventListener(GameEvent.GAME_OVER, gameOver);
					removeChild(gameScreen);
					gameScreen = null;
				break;
				
				case ScreenType.SCORE_SCREEN:
					scoreScreen.menuButton.removeEventListener(TouchEvent.TOUCH_TAP, exitScore);
					scoreScreen.againButton.removeEventListener(TouchEvent.TOUCH_TAP, startGame);
					removeChild(scoreScreen);
					scoreScreen = null;
				break;
			}
		}
		/**
		 * выход из приложения через NativeApplication
		 * при нажатии кнопки "домой" или "назад" приложение закрывается
		 * 
		 * @param	e событие деактивации
		 */
		private function deactivate(e:Event):void {
			NativeApplication.nativeApplication.exit();
		}
		
	}
}


Компиляция и запуск.


Проект настроен, графика нарисована, код написан. Это всё. Можно тестировать игру. Берём смартфон, заходим в Developer Options, включаем USB Debug Mode. Подключаем его кабелем к компьютеру.

Теперь нужно совершить некоторые действия во FlashDevelop.
— компилируем проект Project/BuildProject(F8). В папке bin появится флешка;
— запускаем PackageApp.bat. Вводим «2», нажимаем Enter. Ждём, пока проект запакуется в apk и появится в папке dist;
— в меню выбираем Debug/Start Remote Session;
— Запускаем Run.bat — приложение зальётся на устройство, запустится там и подключится к дебаггеру.

Всё, можно тестировать, использовать дебаггер и даже ловить трейсы прям с устройства. Профайлер, к сожалению, не работает.

Все эти 4 действия можно произвести другим способом. Просто выбираем Project/Test Project(F5). Всё выполнится автоматически.

Когда приложение протестировано и закончено, запускаем PackageApp.bat, вводим единицу. В папке dist появится релизный apk.

Конец первой части.

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


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

Часть вторая
  • +13

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

0
Спасибо. Очень аккуратный код и все досконально прокомментировано — титаническая работа.
Ужасают все эти bat`ы. Только сейчас понимаешь насколько в этом плане Flash Builder круче.
Почему render Mode выбран как CPU?
0
Да, во FlashBuilder работать с мобильным air намного удобней. Об этом я ещё упомяну во второй части.
CPU render mode выбран, потому что для такой простой игры его хватает с головой. (А если честно, на момент написания урока на старой прошивке direct у меня просто не работал)
0
Ещё и GPU есть. Шустрее было бы.
0
В этом проекте я разницы между gpu/cpu/direct не заметил. Там тормозить нечему.
0
1) Air Runtime для Android можно встраивать прямо в apk файл. Ничего не придется скачивать для установки игры
2) Все подобные игры лучше делать с использованием Starling, который шустро работает и имеем дружественный код для AS3 разработчиков
0
Автор, а есть ли гарантия, что GlowFilter, как и остальные — будут работать на Direct/GPU режиме?
0
В gpu фильтры не работают, в direct — работают.
0
1) Я об этом упомянул и во второй части упомяну ещё.
2) Согласен. Лучше даже genome2d, он пошустрей. Но, цитирую
Цель урока — не научить делать игры…, а продемонстрировать основы работы с мобильным AIR.
.
И на момент написания урока AIR 3.2 то ли вообще не было, то ли он был в бэте.
+2
Air с ускорением под андроид далеко не везде работюет.

Например китайские планшеты на AllWinter10 + mali400 и на Amlogic AML 8726-MX + Mali 400MP2 не работают, проверял месяца два назад — и просто air тест, и Starling и тест старлинга с маркета качал и игры про которые знал что air+gpu. Все зависают на этапе инициализации gpu и секунд через 10-15 вываливаются.

Haxe+NME на те-же планшетах работает с ускорением — собирал примеры.

Если у когото есть игры в маркете — кидайте ссылку я запущу проверить.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.