В этой части добавляю:
- медленные персонажи (это будут крупные стрЕлки)
- огибание в пути медленных стрелок быстрыми
- взрывы (с разбрасыванием тел)
Короткая ремарка о стиле написания кода (для читателей первой части):
- не забывайте, код пишется а-ля псевдокод — не смотря на то, что он рабочий, во главу угла поставлена наглядность, а не функциональность и «правильность»
- расширяю базовый класс приложения (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; ... } } ... }
Запускаем. Получаем.

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

Теперь о сложности подхода вообще — работе с Системой Частиц.
Надеюсь, он не показался излишне сложным — «кучи» эмиттеров, настроек к ним, передача частиц между ними…
На самом деле, при data-oriented подходе вся логика поведения сотен частиц заключена именно в эмиттерах. А у нас их сейчас только три (из которых эмиттеры для мелких и крупных стрелок вообще близнецы-братья).
Еще эмиттеры можно представлять в качестве состояний (State) — следование по маршруту и поражение взрывной волной. А «передача» частиц между эмиттерами — ни что иное, как переход между состояниями.
Код доступен на google code. Класс MainWaylines_2
PS: В следующей части добавлю гибель стрелок (ведь взрывы убивают)
поиграюсь с настройками эмиттеров — хочется эпичности.
PPS: Вопрос. Хочу освоить легкий способ создания sprite sheet из анимированных 3D персонажей. Как я для себя это вижу:
- имеется анимированный персонаж
- хочу в некоем программном продукте задать примерно следующие параметры:
- размер
- угол камеры
- число фреймов
- на выходе — sprite sheet
Не подскажете, в какую сторону смотреть? Может есть ПОДРОБНОЕ описание подобной РАБОЧЕЙ методики?
Заранее спасибо.
ссылка на оригинал статьи http://habrahabr.ru/post/218697/

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