Pull to refresh

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

Reading time9 min
Views7.2K
Продолжаем разговор от 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
Не подскажете, в какую сторону смотреть? Может есть ПОДРОБНОЕ описание подобной РАБОЧЕЙ методики?
Заранее спасибо.

PPPS: добавил две строчки в код метода MainWaylines_2.explosion(): обнуляю векторы скоростей частицы перед взрывом — естественней смотрится

protected function explosion(e:MouseEvent):void
{	
	...
			particleClone.velX = 0;
			particleClone.velY = 0;
	...				
}

Tags:
Hubs:
Total votes 3: ↑3 and ↓0+3
Comments0

Articles