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

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

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

    Поделиться публикацией
    Комментарии 0

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

    Самое читаемое