Вариант построения архитектуры приложения на API Alternativa 7

Материал из AlternativaPlatform Wiki

Перейти к: навигация, поиск

Работа с большим количеством объектов пораждает массу запутанного кода. И с ростом приложения, эта путаница только усугубляется. Поддерживать и развивать проект становится просто невыносимо. Для этих целей спроектировать своё приложение сразу и заложить в его архитектуру удобство доступа к объектам будет далеко не лишним. Давайте попробуем это сделать для приложений, использующих API Alternativa 7 — классов, реализующих трехмерное представление во Flash.


Трехмерное пространство в Альтернативе состоит из камеры, плоскости проекции, контейнера для 3-мерных объектов и непосредственно самих 3-мерных объектов. Камера — это некая абстракция, моделирующая точку обзора сцены в мировых координатах и луч обзора. Камера представлена классом Camera3D. Думаю, назначение камеры понятно каждому. Плоскость проекции представлена классом View. Эта плоскость необходима для проекции отображаемых объектов. То, что проецируется на неё и представляет собой то, что мы с вами видим после растеризации. Контейнер 3-мерных объектов представлен в Альтернативе несколькими классами, но в этом уроке мы будем использовать контейнер ConflictContainer. Контейнеры в Альтернативе предназначены для реализации математики отсечения, отбраковки, z-буфера — всего того, что необходимо для корректного отображения 3-мерных объектов сцены. Алгоритмы для разных контейнеров используются разные. Так задумано. Какие-то из них наиболее точные, другие более грубые, но отрабатывают быстрее. Все трехмерные объекты сцены должны располагаться в контейнерах. Трехмерные объекты — наследники класса Object3D. Их несколько, но в этом уроке мы воспользуемся подклассом Mesh, реализующим геометрию трехмерного объекта посредством точек (vertex), ребер (edge) и граней (face). А еще точнее мы будем использовать подкласс Box класса Mesh. Класс Box - это уже готовый примитив, представляющий собой прямоугольный параллелепипед. Логика нашего приложения будет разделена на две части. В одной части (классе ApplicationManager) будет храниться логика, относящаяся к самому приложению. В задачи этого класса может входить инициализация объектов трехмерной сцены, их добавление и управление анимацией этих объектов. В результате нам не нужно будет выискивать точки управления объектами сцены, а мы можем их очень легко найти в этом единственном классе. Во второй части (классе EngineManager) будет сосредоточена логика, необходимая для работы нашего 3-мерного движка. Эта архитектура позаимствована из уроков Мэтью Касперсена (оригинал которых можно найти здесь, а перевод первых трех его уроков - здесь). Архитектура показалась мне очень продуманной и оптимальной для построения приложения. Чтобы создать наш первый трехмерный мир, нам надо добавить его в список отображения, т.е. наш класс EngineManager будет наследовать свойства DisplayObjectContainer и в нем будут инициироваться все объекты, относящиеся к работе движка. К слову сказать, EngineManager спроектирован как синглтон. Этот паттерн программирования подстрахует нас от создания еще одного объекта EngineManager. Почему нам необходимо ограничить самих себя таким образом? Всё достаточно просто. На работу трехмерного приложения в подавляющем большинстве случаев разработчиком выделяется максимально-возможное количество ресурсов компьютера: процессорного времени, оперативной памяти, т.к. расчеты трехмерных объектов весьма затратны. Если в процессе разработки мы вдруг допустим ошибку и создадим еще один объект класса EngineManager, то мы неоправданно ограничим себя в столь ценных ресурсах! А не хотелось бы. Итак, EngineManager — синглтон.
package  
{
 
	import alternativa.engine3d.core.Camera3D;
	import alternativa.engine3d.core.View;
	import alternativa.engine3d.core.Object3D;
	import alternativa.engine3d.containers.ConflictContainer;
	import alternativa.engine3d.controllers.SimpleObjectController;
 
	import flash.display.Sprite;
	import flash.display.StageAlign;
	import flash.display.DisplayObject;
	import flash.events.Event;
	import flash.utils.getTimer;
	import flash.geom.Vector3D;
	import mx.collections.ArrayCollection;
 
 
	/**
	 * 	EngineManager содержит код, относящийся к движку Alternativa3D.
	 */
	public class EngineManager extends Sprite
	{
		private static var _instance:EngineManager = null;
		private var _container:ConflictContainer;
		private var _camera:Camera3D;
		private var _controller:SimpleObjectController;
		private var _lastTime:int;
		private var _cameraTarget:Vector3D;
 
		private var _baseObjects:ArrayCollection = new ArrayCollection();
		private var _newBaseObjects:ArrayCollection = new ArrayCollection();
		private var _removedBaseObjects:ArrayCollection = new ArrayCollection();
 
		public static function get instance ():EngineManager {
			if (_instance == null)
				_instance = new EngineManager();
			return _instance;
		}
 
		public function EngineManager () {
			if (stage != null) return;
			addEventListener(Event.ADDED_TO_STAGE, create);
		}
 
		/**
		 * Свойство для доступа к нашему контейнеру 3-мерной сцены.
		 */
		public function get scene ():ConflictContainer {
			return _container;
		}
 
		/**
		 * Свойство для доступа к диаграмме состояния камеры
		 */
		public function get diagram ():DisplayObject {
			return _camera.diagram;
		}
 
		/**
		 * Точка, куда направлен луч камеры.
		 */
		public function set cameraTarget (target:Vector3D):void {
			_cameraTarget = target;
		}
 
		/**
		 * Создание нашего 3-мерного пространства.
		 */
		private function create (e:Event):void {
			removeEventListener(Event.ADDED_TO_STAGE, create);
 
			// точка пространства, куда направлена камера
			_cameraTarget = new Vector3D(0, 0, 0);
 
			// контейнер для 3-мерных объектов
			_container = new ConflictContainer();
 
			// создание области проекции и камеры
			_camera = new Camera3D();
			_camera.view = new View(stage.stageWidth, stage.stageHeight);
			_camera.x = -200;
			addChild(_camera.view);
 
			_container.addChild(_camera);
 
			// создание простейшего контроллера клавы и мыши
			_controller = new SimpleObjectController(stage, _camera, 100, 3, .05);
			_controller.lookAt(_cameraTarget);
 
			// регистрация обработчиков основных событий
			addEventListener(Event.ENTER_FRAME, enterFrameListener);
			addEventListener(Event.RESIZE, resizeStageListener);
			resizeStageListener(null);
 
			_lastTime = getTimer();
 
			// здесь запускается логика приложения
			ApplicationManager.instance.startupApplicationManager();
		}
 
		/**
		 * Обработчик события перерисовки кадра.
		 */
		private function enterFrameListener (e:Event):void {
			// время, просшедшее с последнего просчета кадра
			var currentTime:int = getTimer();
			var deltaTime:Number = (currentTime - _lastTime) / 1000.0;
	    	_lastTime = currentTime;
 
	    	// синхронизация всех созданных или удаленных BaseObject-ов в коллекциях
			// во время просчета сцены
	    	removeDeletedBaseObjects();
	    	insertNewBaseObjects();
 
	    	// самообновление BaseObject-объекта
	    	for each (var baseObject:BaseObject in _baseObjects)
	    		baseObject.enterFrame(deltaTime);
 
			// считывание параметров контроллером
			_controller.update();
 
			// просчет сцены. сердце нашего 3-мерного процессора!
			_camera.render();
		}
 
		/**
		 * Обработчик события изменения размеров кадра.
		 */
		private function resizeStageListener (e:Event):void {
			_camera.view.width = stage.stageWidth;
			_camera.view.height = stage.stageHeight;
		}
 
		/**
		 * Добавление объектов BaseObject в буффер-коллекцию добавления.
		 */
		public function addBaseObject(baseObject:BaseObject):void {
			_newBaseObjects.addItem(baseObject);
		}
 
		/**
		 * Добавление объектов BaseObject в буффер-коллекцию удаления.
		 */		
		public function removeBaseObject(baseObject:BaseObject):void {
			_removedBaseObjects.addItem(baseObject);
		}
 
		/**
		 * Удаление объектов BaseObject со сцены.
		 */	
		private function shutdownAll():void {
			for each (var baseObject:BaseObject in _baseObjects)
			{
				var found:Boolean = false;
				for each (var removedObject:BaseObject in _removedBaseObjects)
				{
					if (removedObject == baseObject)
					{
						found = true;
						break;
					}
				}
 
				if (!found)
					baseObject.shutdown();
			}
		}
 
		/**
		 * Добавление объектов BaseObject из буффер-коллекции добавления в основную коллекцию
		 * и очистка буффер-коллекции.
		 */	
		private function insertNewBaseObjects():void {
			for each (var baseObject:BaseObject in _newBaseObjects)
				_baseObjects.addItem(baseObject);
 
			_newBaseObjects.removeAll();
		}
 
		/**
		 * Удаление объектов BaseObject из основной коллекции
		 * и очистка буффер-коллекции удаления.
		 */	
		private function removeDeletedBaseObjects():void {
			for each (var removedObject:BaseObject in _removedBaseObjects)
			{
				var i:int = 0;
				for (i = 0; i < _baseObjects.length; ++i)
				{
					if (_baseObjects.getItemAt(i) == removedObject)
					{
						_baseObjects.removeItemAt(i);
						break;
					}
				}
 
			}
 
			_removedBaseObjects.removeAll();
		}
 
	}
 
}

В конструкторе класса EngineManager мы подписываемся на событие добавления объекта EngineManager на сцену, потому как пока объект не добавлен, его свойство stage равно null. А это свойство нам понадобится внутри объекта. После того как объект добавлен, начинается самое интересное. Хотя код подробно закомментирован, всё же пройдемся по нему. В методе create мы создаем все основные объекты движка и добавляем их в EngineManager, наследник DisplayObjectContainer: контейнер 3-мерных объектов ConflictContainer, камеру Camera3D, плоскость проекции View, контроллер устройств ввода SimpleObjectController (для облегчения обработки пользовательского ввода, чтобы двигать нашу камеру) и запускаем наше приложение в строке ApplicationManager.instance.startupApplicationManager().

Также мы регистрируем пару важнейших для большинства приложений Альтернативы обработчиков событий на изменение размеров окна флеш-плеера и перерисовки кадра. При изменении размеров мы должны пересчитать окно проекции, а при перерисовке кадра происходит растеризация 3-мерной сцены. Здесь — сердце нашего приложения — в строке camera.render().

Но перед тем как начать просчет сцены надо провести ряд подготовительных мероприятий. Во-первых надо высчитать время после завершения просчета предыдущего кадра. Это время мы будем использовать для анимации визуализируемых объектов, чтобы не быть привязанным к реальной частоте просчета. Т.е. если просчет предыдущего кадра занял больше времени, то и трансформация анимируемого объекта (смещение, вращение, масштабирование) будет увеличена пропорционально. Таким образом не будет заметна разница во времени перерисовки кадров и анимация будет плавной.

Затем надо синхронизировать список отображаемых объектов. Для этого предназначены 2 инструкции: removeDeletedBaseObjects() и insertNewBaseObjects(). После того как объекты синхронизированы мы можем вызвать их метод enterFrame, принимающий секунды прошедшие с окончания просчета предыдущего кадра, для того чтобы анимировать объект, если он того требует. И, наконец, необходимо обработать сигналы контроллеров (клавиатуры и мыши). Это просто: controller.update(). Теперь можно считать наш кадр.

Мы рассмотрели место, где происходит основная работа движка, теперь уделим внимание "переферии" — логике самого приложения. Помните, где она стартует? В строке ApplicationManager.instance.startupApplicationManager(). ApplicationManager также спроектирован как синглтон (по тем же причинам, что и EngineManager) и предназначен для реализации логики приложения. В этом уроке логики не много и она тривиальна: мы создаем примитив RotatingBox на основе стандартного класса Альтернативы Box и созданного нами класса MeshObject и направляем "луч зрения" камеры на его центр.

package
{
 
	import flash.geom.Vector3D;
 
	/**
	 * 	ApplicationManager содержит код, относящийся к логике всего приложения.
	 */
	public class ApplicationManager
	{
		protected static var _instance:ApplicationManager = null;
 
		public static function get instance():ApplicationManager {
			if (_instance == null)
				_instance = new ApplicationManager();
			return _instance;
		}
 
		public function ApplicationManager() { }
 
		/**
		 * Запуск логики приложения.
		 */
		public function startupApplicationManager():ApplicationManager {
			var rotatingBox:RotatingBox = new RotatingBox().startupRotatingBox();
			EngineManager.instance.cameraTarget = new Vector3D(rotatingBox.model.x, rotatingBox.model.y, rotatingBox.model.z);
 
			return this;
		}
 
	}
}

Теперь разберем более подробно наш не сложный RotatingBox. RotatingBox наследуется от более общего класса BaseObject — основы наших трехмерных объектов. Класс BaseObject мы ввели для того, чтобы было легче управлять трехмерными объектами сцены: добавлять и удалять их в коллекциях и на сцене, синхронно анимировать. Для этого в классе введены соответствующие методы startupBaseObject(object:Object3D):void, shutdown():void, enterFrame(dt:Number):void. Их функциональность достаточно прозрачна. Также в класс BaseObject заложено свойство model:Object3D, несущее информацию о геометрии объекта. Для того, чтобы создать объект определенной формы будет достаточно присвоить этому свойству ссылку на объект класса наследника Object3D и геометрия готова.

package
{
	import alternativa.engine3d.core.Object3D;
 
	/**
	 *	BaseObject обновляет сам себя во время основного цикла растеризации. 
	 */
	public class BaseObject
	{
		public var model:Object3D = null;
 
		public function BaseObject() { }
 
		/**
		 * Должен быть вызван всеми дочерними классами после создания.
		 * Добавляет модель в 3D-пространство.
		 * Помещает объект в коллекцию BaseObjects содержащуюся в классе EngineManager.
		 */
		public function startupBaseObject(object:Object3D):void {
			model = object;
			EngineManager.instance.scene.addChild(model);
			EngineManager.instance.addBaseObject(this);
		}
 
		/**
		 * Должен быть вызван всеми дочерними классами после уничтожения.
		 * Удаляет модель из 3D-пространства.
		 * Удаляет объект из коллекции BaseObjects содержащейся в классе EngineManager.
		 */
		public function shutdown():void {
			EngineManager.instance.removeBaseObject(this);
			EngineManager.instance.scene.removeChild(model);
			model = null;
		} 
 
		/**
		 * 	Эта функция вызввается каждый кадр перед просчетом сцены.
		 * 
		 * 	@param dt - время в секундах с последнего просчета кадра сцены.
		 */
		public function enterFrame(dt:Number):void {
			// for override
		}
	}
}

Основа для наших геометрических объектов создана. Теперь — последний штрих. Создаем модель куба: var box:Box = new Box(100, 100, 100, 3, 3, 3) и применяем к ней простой материал box.setMaterialToAllFaces(new FillMaterial(0x000000, 0, 1, 0x666666)). Создаем код для анимации куба. Он находится в методе enterFrame. Мы просто поворачиваем наш бокс каждый кадр на небольшую виличину, выравненную по принимаемому параметру dt — времени от окончания просчета предыдущего кадра.

package
{
	import alternativa.engine3d.materials.FillMaterial;
	import alternativa.engine3d.primitives.Box;
 
	public class RotatingBox extends BaseObject
	{
		protected static const ROTATION_SPEED:Number = 1;
 
		public function RotatingBox() {
			super();
		}
 
		/**
		 * Создание примитива.
		 */
		public function startupRotatingBox ():RotatingBox {
			var box:Box = new Box(100, 100, 100, 3, 3, 3);
			box.setMaterialToAllFaces(new FillMaterial(0x000000, 0, 1, 0x666666));
			super.startupBaseObject(box);
			return this;
		}
 
		/**
		 * Анимация примитива.
		 */
		public override function enterFrame (dt:Number):void {
			model.rotationX += dt * ROTATION_SPEED;
			model.rotationY += dt * ROTATION_SPEED;
			model.rotationZ += dt * ROTATION_SPEED;
		}
	}
}

Инициируем наше приложение в корневом классе ApplicationExample. Для мониторинга использования ресурсов можно добавить диаграмму состояния движка Альтернативы: addChild(engine.diagram)

package 
{
 
	import flash.display.Sprite;
	import flash.events.Event;
 
 
	[SWF(width="800", height="600", frameRate="60", backgroundColor="#000000")]
	public class ApplicationExample extends Sprite 
	{
		private var engine:EngineManager;
 
 
		public function ApplicationExample ():void {
			if (stage) initialize();
			else addEventListener(Event.ADDED_TO_STAGE, initialize);
		}
 
		private function initialize (e:Event = null):void {
			removeEventListener(Event.ADDED_TO_STAGE, initialize);
 
			engine = EngineManager.instance;
			addChild(engine);
			addChild(engine.diagram);
		}
 
	}
 
}

Всё, скелет приложения готов. И даже добавлен анимированный куб для примера. Стрелки влево-вправо, клавиши A и D, мышь с зажатой левой кнопкой поворачивают камеру, стрелки вверх-вниз, клавиши W и S меняют её угол обзора. Это интерактивное взаимодействие обеспечивает объект класса SimpleObjectController, который мы добавили в EngineManager. Без него мы бы смогли наблюдать только за вращением нашего кубика.

Код урока можно загрузить отсюда.

Личные инструменты
Пространства имён
Варианты
Действия
Навигация
Category
Инструменты
На других языках