Particles System в моделировании толпы (2)

от автора

Продолжаем разговор от 07.04.2014 (Particles System в моделировании толпы).

В этой части добавляю:

  1. медленные персонажи (это будут крупные стрЕлки)
  2. огибание в пути медленных стрелок быстрыми
  3. взрывы (с разбрасыванием тел)


Короткая ремарка о стиле написания кода (для читателей первой части):

  • не забывайте, код пишется а-ля псевдокод — не смотря на то, что он рабочий, во главу угла поставлена наглядность, а не функциональность и «правильность»
  • расширяю базовый класс приложения (MainWailines_1) через класс (MainWailines_2) — опять же ради наглядности, и чтоб не смешивать комментарии из разных статей (т.е. — в реальности, конечно, я бы не игрался с наследованием в этом случае)
  • комментарии из первой части во второй части удаляю — все ради того же, да-да, ради наглядности
  • картинок много — проиллюстрировать эволюцию решения и показать, что нет предела совершенству
медленные персонажи

Пишем метод MainWaylines_2.setupEmitterForMonsterArrows(). Фактически это copy-paste прежнего MainWaylines_1.setupEmitter(). Я только удалил старые комментарии, и оставил их лишь там, где есть изменения.

protected function setupEmitterForMonsterArrows():void { 	var emitter:Emitter2D = new Emitter2D(); 	// это счетчик - устанавливаем на 1 Чудовищную Стрелку в секунду 		emitter.counter = new Steady(1); 	 		var wayline:Wayline = _waylines[0]; 		emitter.addInitializer( new Position( new LineZone( new Point(wayline.x - wayline.radius*Math.cos(wayline.rotation), wayline.y - wayline.radius*Math.sin(wayline.rotation)), new Point(wayline.x + wayline.radius*Math.cos(wayline.rotation), wayline.y + wayline.radius*Math.sin(wayline.rotation)) ) ) ); 	// сообщаем, какую картинку использовать рендеру при отрисовке частицы 	// делаем юнитов покрупнее 		emitter.addInitializer( new ImageClass( Arrow, [10] ) ); 		 		emitter.addAction( new DeathZone( new RectangleZone( -30, -30, stage.stageWidth+60, stage.stageHeight + 60 ), true ) ); 		emitter.addAction( new Move() ); 		emitter.addAction( new RotateToDirection() ); 	// если юнитов этого типа будет мало, и между ними будет большое расстояние, 	// то можно было бы вообще исключить этот action	 	//	emitter.addAction( new MinimumDistance( 7, 600 ) );			 	 	// делаем юнитов помедленнее 		emitter.addAction( new ActionResistance(.1)); 	 		emitter.addAction( new FollowWaylines(_waylines) );    	    	var	renderer:DisplayObjectRenderer = new DisplayObjectRenderer();    		addChild( renderer );    		renderer.addEmitter( emitter ); 	// командуем старт 		emitterWaylinesForMonsterArrows = emitter;			 		emitterWaylinesForMonsterArrows.start(); } 

теперь расширяем и запускаем MainWaylines_2.setup()

override protected function setup(e:Event=null):void { 	super.setup(); 	 	// создаем новый эмиттер для крупных и медленных 	setupEmitterForMonsterArrows(); } 

получаем картинку, подобную этой. Крупные стрелки сливаются с мелкими — существуют параллельно

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

для того, чтоб мелочь огибала крупные стрелки, нужно им дать команду. Добавляем строчку в MainWaylines_2.setup(), где Antigravities — это еще один стандартный action из библиотеки системы частиц (классная библиотека, да?).

override protected function setup(e:Event=null):void { 	super.setup(); 	 	// создаем новый эмиттер для крупных и медленных 	setupEmitterForMonsterArrows(); 	// добавляем новый action к эмиттеру "для самых маленьких"         // обратите внимание(!) эмиттер УЖЕ запущен, и его не надо перезапускать - поведение частиц можно менять на лету 	emitterWaylines.addAction( new Antigravities(emitterWaylinesForMonsterArrows, -400000) ); } 

и результат начинает походить вот на такую картинку. Мелкие стрелки уже огибают крупные, но уж очень много их скапливается позади. Эти пробки совсем некрасиво смотрятся.

это происходит из-за следующего «конфликта». Antigravities заставляет мелкие стрелки огибать крупные. Одновременно с этим гонит их вперед FollowWaylines — каждая стрелка стремится к определенной точке на перпендикуляре пути, помните? Мелкие стрелки просто не успевают воврмя обогнуть крупную из-за того что слишком быстро приближаются к узловым точкам на пути. Одно из решений (и мне кажется, самое простое) — это увеличение длины отрезков пути (расстояния между узлами маршрута).

переписываем MainWaylines_2.setupWaylines() ради одной строчки

override protected function setupWaylines():void { 	_waylines = []; 	 	var w:Number = stage.stageWidth; 	var h:Number = stage.stageHeight; 	var points:Array = [new Point(-9,h*.4), new Point(w*.3,h*.4), new Point(w*.5,h*.1), new Point(w*.8,h*.1), new Point(w*.8,h*.9), new Point(w*.5, h*.9), new Point(w*.3, h*.8), new Point(-40, h*.8)]; 	var fitline:FitLine = new FitLine(points); 	var path:Path = new Path(fitline.fitPoints); 	 	/* 	 * переписываем одно число. Было 40, станет 25 	 *  	 * более красивым решением, было бы написание метода, который расчитывал бы число шагов в зависимости от длины пути 	 * ну, это надо лишь, если мы планируем автоматически создавать много разных маршрутов 	 */ 	var step:Number = path.length / 25; 	 	var strength:Number = 100; 	for(var i:int=0; i<path.length; i+=step) 	{ 		var segmentLength:int = 60;//*Math.random()+10; 		var pathpoint:PathPoint = path.getPathPoint(i); 		var wayline:Wayline = new Wayline(pathpoint.x, pathpoint.y, segmentLength, pathpoint.rotation-Math.PI/2, strength); 		_waylines.push(wayline); 	} } 

А еще, раз крупных стрелок существенно меньше мелких (в 60 раз), их можно пустить по более узкому фарватеру (уменьшить ширину ЭМИТТЕРА для крупных стрелок), и тем самым дать мелким стрелкам возможность обходить их с краю свободнее.

редактируем MainWaylines_2.setupEmitterForMonsterArrows(), уменьшив LineZone эмиттера на 20 (по 10 пикселей с каждой стороны)

emitter.addInitializer( new Position( new LineZone( new Point(wayline.x - (wayline.radius-10)*Math.cos(wayline.rotation), wayline.y - (wayline.radius-10)*Math.sin(wayline.rotation)), new Point(wayline.x + (wayline.radius-10)*Math.cos(wayline.rotation), wayline.y + (wayline.radius-10)*Math.sin(wayline.rotation)) ) ) ); 

теперь пробки за крупными стрелками стали значительно меньше

взрывы (с разбрасыванием тел)

Создаем новый эмиттер — для анимации разбрасывания тел

protected function setupEmitterForExplosion():void { 	var emitter:Emitter2D = new Emitter2D(); 	// чтоб частицы двигались - это уже знакомо 		emitter.addAction( new Move() ); 	// чтоб не играться с соотношениями сил, чтоб не очень быстро разбрасывались частицы - проще тупо ограничить скорость				 		emitter.addAction( new SpeedLimit(40)); 	// это чтоб частицы постепенно замедлялись - трение 		emitter.addAction( new Friction(40) ); 	// на всякий случай - вдруг вылетят (хотя можно было на другие эмиттеры оставить) 		emitter.addAction( new DeathZone( new RectangleZone( -30, -10, stage.stageWidth+40, stage.stageHeight + 20 ), true ) ); 	// новый рендер 	var	renderer:DisplayObjectRenderer = new DisplayObjectRenderer();    		addChild( renderer );    		renderer.addEmitter( emitter ); 	// командуем старт 		emitterExplosion = emitter;	 		emitterExplosion.start(); } 

Подписываемся на MouseEvent.MOUSE_DOWN в MainWaylines_2.setup() — по этим событиям будем генерировать взрывы

stage.addEventListener(MouseEvent.MOUSE_DOWN, handleMouseDown); 

почему не сразу вызываем explosion(e);? Туда можно анимацию самого взрыва добавить, по окончании которой сгенерить последствия

private function handleMouseDown(e:MouseEvent):void { 	explosion(e); } 

Теперь сам взрыв

private function explosion(e:MouseEvent):void {	 	if(emitterWaylines == null){ return; } 	if(emitterExplosion == null){ return; } 	 	// радиус взрыва 	var explRadius:int = 30; 	// ради оптимизации заводим локальные переменные  	// (внутри больших циклов обращение к данным не на прямую, а через dot-синтаксис начинает существенно потреблять процессорное время) 	var particleOrigin:Particle2D; 	var particleClone:Particle2D; 	var particlePoint:Point = new Point(); 	 	// произошел взрыв в точке... 	var explPoint:Point = new Point(e.stageX, e.stageY); 	// готовимся к длинному циклу 	var particles:Array = emitterWaylines.particlesArray; 	var length:int = particles.length; 	// перебор всех частиц в эмиттере 	for(var p:int=0; p<length; p++) 	{ 		particleOrigin = particles[p]; 		particlePoint.x = particleOrigin.x; 		particlePoint.y = particleOrigin.y; 		// проверка, попадают ли частицы в радус действия взрыва 		if(Point.distance(explPoint, particlePoint) < explRadius) 		{ 			/* 			 * клонируем частицу, которую накрыло взрывом - ее клон надо будет поместить в эмиттер взрывов 			 * и задаем ей небольшой импульс вращения - имитируем потерю контроля 			 */ 			particleClone = particleOrigin.clone(emitterExplosion.particleFactory) as Particle2D; 			particleClone.angVelocity = -5 + Math.random() * 10; 			/* 			 * создаем новый экземпляр Arrow (красного цвета) - ведь объкты в ActionScript не копируются, а передается ссылка на них 			 * ВАЖНО! если копии не передать новую картинку,  			 * то при удалении оригинальной частицы из прежнего эмиттера emitterWaylines сгенерится ошибка  			 * - потому что рендер не сможет выполнить renderer.removeChild() 			 *  			 * это ведь только прототип. И родной рендер используется только для визуализации процессов.  			 * В реальной игре вы можете (и будете, наверняка) использовать сторонние рендеры,  			 * и оперировать будете только координатами частиц (кстати - вот еще один важный пункт оптимизации)  			 */ 			particleClone.image = new Arrow(4, 0xff0000); 			// добавляем клонированную частицу в эмиттер взрывов 			emitterExplosion.addParticle(particleClone); 			// убираем частицы из старого эмиттера 			particleOrigin.isDead = true; 		} 	} 	 	/* 	 * добавляем action в эмиттер взрывов 	 *  	 * на самом деле, конечно, подход неоднозначный - можно было бы сначала проверить,  	 * зацепило ли кого взрывом, а потом уже создавать эмиттер и активировать его (т.е экономим на создании экзмпляра эмиттера). 	 *  	 * с другой стороны - пришлось бы два цикла запускать: поиск и закгрузка в новый эмиттер 	 *  	 * а может, в будущей игре взрывы возможны только в толпе, тогда первый вариант верный...  	 * в общем - тут нужна комплексная оценка 	 */ 	var explosion:Explosion = new Explosion(10000, explPoint.x, explPoint.y, 100); 	emitterExplosion.addAction(explosion); 	 	/* 	 * нам нужно чтоб взрыв воздействовал на частицу короткое время - чтоб ее не унесло за тридевять земель 	 * для этого надо ОДИН раз вызывать Emitter2D.update(.2) - чтоб частицы получили нужное ускорение 	 */			 	// задаем ускорение частицам в зоне взрыва внутри эмиттера 	emitterExplosion.update(0.2); 	// удаляем action Explosion  - он уже не нужен 	emitterExplosion.removeAction(explosion);			 } 

Запускаем. Через несколько кликов получаем следущую картинку — красные бесконтрольно накапливаются, а ведь их нужно возвращать обратно в поток.

Суть необходимых изменений проста — по истечении определенного времени надо «возвращать» частицу в прежний поток.
1. Сначала вносим изменения в MainWaylines_2.setupEmitterForExplosion():

protected function setupEmitterForExplosion():void { 	var emitter:Emitter2D = new Emitter2D(); 	... 	// этот action отсчитывает "возраст" частицы. По истечению возраста, частица удаляется. 	// соотв. надо подписаться на событие, чтоб вернуть частицу в прежний эмиттер  		emitterExplosion.addAction( new Age() ); 	... 	// подписываемся на "смерть частицы от старости", чтоб перенести ее обратно в "родной" эмиттер 	 	emitterExplosion.addEventListener(ParticleEvent.PARTICLE_DEAD, handleParticleDeadFromEmitterExplosion); } 

2. теперь добавляем изменения в MainWaylines_2.explosion()

private function explosion(e:MouseEvent):void {	 	... 	// перебор всех частиц в эмиттере 	for(var p:int=0; p<length; p++) 	{ 		... 		// проверка, попадают ли частицы в радус действия взрыва 		if(Point.distance(explPoint, particlePoint) < explRadius) 		{ 			particleClone = particleOrigin.clone(emitterExplosion.particleFactory) as Particle2D; 			particleClone.angVelocity = -5 + Math.random() * 10; 			/* 			 * action Age() в эмиттере взрывов, будет обрабатывать возраст частицы 			 * и когда возраст сравняется с заявенным временм жизни, она "умрет" 			 * тогда обработчик перехватит сообщение о смерти и перенесет частицу обратно 			 */ 			particleClone.lifetime = 3; 			particleClone.age = 0;                        ... 		} 	} 	... } 

Запускаем. Получаем.

Итог:

  1. два типа юнитов: мелкие и крупные
  2. мелкие юниты огибают крупные
  3. взрывы действют на мелкие юниты (пусть это будет шрапнель, которая не действует на танки — крупные стрелки)
  4. после того, как мелкие оправятся от «кантузии», они снова возвращаются в общий поток

Очевидные минусы

  1. не-эпично высокая скорость стрелок
  2. низкий FPS

Если для решения проблемы с п.1. можно продолжить играться с настройками эмиттеров (а сегодняшний мой способ использования системы частиц не самый совершенный), то что же с п.2.(FPS)? Есть ли потенциал для оптимизации? Ведь надо же еще графику нормальную прикручивать, еще кучу игрового кода…

Думаю, потенциал для оптимизации есть, и немалый

  1. Запрет на столкновения между мелкими стрелками, при текущих масштабах — на самом деле чистая блажь — можно число юнитов увеличить в 2-5 раз, и в образовавшейся каше вообще ничего не разглядеть будет (а если проекция на поле не top-down, как сейчас, а изометрическая?). Да и не будет «полной каши» — ведь мелкие стрелки, не забывайте, двигаются по индивидуально заданным маршрутам (у каждой имеется свое положение относительно перпендикуляра к касательной). Попробуйте отключить action MinimumDistance, предупреждающий взаимные столкновения — особой разницы не заметите (только при обгоне крупных). А прирост в производительности — существенный (можете глянуть в код action-а и увидеть, СКОЛЬКО там расчетов).
  2. Просто отключил «родной» рендер — и FPS сразу подрос в более, чем в полтора раза (а если на Starling).

Теперь о сложности подхода вообще — работе с Системой Частиц.
Надеюсь, он не показался излишне сложным — «кучи» эмиттеров, настроек к ним, передача частиц между ними…
На самом деле, при data-oriented подходе вся логика поведения сотен частиц заключена именно в эмиттерах. А у нас их сейчас только три (из которых эмиттеры для мелких и крупных стрелок вообще близнецы-братья).
Еще эмиттеры можно представлять в качестве состояний (State) — следование по маршруту и поражение взрывной волной. А «передача» частиц между эмиттерами — ни что иное, как переход между состояниями.

Код доступен на google code. Класс MainWaylines_2

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

PPS: Вопрос. Хочу освоить легкий способ создания sprite sheet из анимированных 3D персонажей. Как я для себя это вижу:

  1. имеется анимированный персонаж
  2. хочу в некоем программном продукте задать примерно следующие параметры:
    • размер
    • угол камеры
    • число фреймов
  3. на выходе — sprite sheet

Не подскажете, в какую сторону смотреть? Может есть ПОДРОБНОЕ описание подобной РАБОЧЕЙ методики?
Заранее спасибо.

ссылка на оригинал статьи http://habrahabr.ru/post/218697/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *