Продолжаем разговор от 07.04.2014 (Particles System в моделировании толпы).
В этой части добавляю:
Короткая ремарка о стиле написания кода (для читателей первой части):
Пишем метод MainWaylines_2.setupEmitterForMonsterArrows(). Фактически это copy-paste прежнего MainWaylines_1.setupEmitter(). Я только удалил старые комментарии, и оставил их лишь там, где есть изменения.
теперь расширяем и запускаем MainWaylines_2.setup()
получаем картинку, подобную этой. Крупные стрелки сливаются с мелкими — существуют параллельно
для того, чтоб мелочь огибала крупные стрелки, нужно им дать команду. Добавляем строчку в MainWaylines_2.setup(), где Antigravities — это еще один стандартный action из библиотеки системы частиц (классная библиотека, да?).
и результат начинает походить вот на такую картинку. Мелкие стрелки уже огибают крупные, но уж очень много их скапливается позади. Эти пробки совсем некрасиво смотрятся.
это происходит из-за следующего «конфликта». Antigravities заставляет мелкие стрелки огибать крупные. Одновременно с этим гонит их вперед FollowWaylines — каждая стрелка стремится к определенной точке на перпендикуляре пути, помните? Мелкие стрелки просто не успевают воврмя обогнуть крупную из-за того что слишком быстро приближаются к узловым точкам на пути. Одно из решений (и мне кажется, самое простое) — это увеличение длины отрезков пути (расстояния между узлами маршрута).
переписываем MainWaylines_2.setupWaylines() ради одной строчки
А еще, раз крупных стрелок существенно меньше мелких (в 60 раз), их можно пустить по более узкому фарватеру (уменьшить ширину ЭМИТТЕРА для крупных стрелок), и тем самым дать мелким стрелкам возможность обходить их с краю свободнее.
редактируем MainWaylines_2.setupEmitterForMonsterArrows(), уменьшив LineZone эмиттера на 20 (по 10 пикселей с каждой стороны)
теперь пробки за крупными стрелками стали значительно меньше
Создаем новый эмиттер — для анимации разбрасывания тел
Подписываемся на MouseEvent.MOUSE_DOWN в MainWaylines_2.setup() — по этим событиям будем генерировать взрывы
почему не сразу вызываем explosion(e);? Туда можно анимацию самого взрыва добавить, по окончании которой сгенерить последствия
Теперь сам взрыв
Запускаем. Через несколько кликов получаем следущую картинку — красные бесконтрольно накапливаются, а ведь их нужно возвращать обратно в поток.
Суть необходимых изменений проста — по истечении определенного времени надо «возвращать» частицу в прежний поток.
1. Сначала вносим изменения в MainWaylines_2.setupEmitterForExplosion():
2. теперь добавляем изменения в MainWaylines_2.explosion()
Запускаем. Получаем.
Итог:
Очевидные минусы
Если для решения проблемы с п.1. можно продолжить играться с настройками эмиттеров (а сегодняшний мой способ использования системы частиц не самый совершенный), то что же с п.2.(FPS)? Есть ли потенциал для оптимизации? Ведь надо же еще графику нормальную прикручивать, еще кучу игрового кода…
Думаю, потенциал для оптимизации есть, и немалый
Теперь о сложности подхода вообще — работе с Системой Частиц.
Надеюсь, он не показался излишне сложным — «кучи» эмиттеров, настроек к ним, передача частиц между ними…
На самом деле, при data-oriented подходе вся логика поведения сотен частиц заключена именно в эмиттерах. А у нас их сейчас только три (из которых эмиттеры для мелких и крупных стрелок вообще близнецы-братья).
Еще эмиттеры можно представлять в качестве состояний (State) — следование по маршруту и поражение взрывной волной. А «передача» частиц между эмиттерами — ни что иное, как переход между состояниями.
Код доступен на google code. Класс MainWaylines_2
PS: В следующей части добавлю гибель стрелок (ведь взрывы убивают)
поиграюсь с настройками эмиттеров — хочется эпичности.
PPS: Вопрос. Хочу освоить легкий способ создания sprite sheet из анимированных 3D персонажей. Как я для себя это вижу:
Заранее спасибо.
PPPS: добавил две строчки в код метода MainWaylines_2.explosion(): обнуляю векторы скоростей частицы перед взрывом — естественней смотрится
В этой части добавляю:
- медленные персонажи (это будут крупные стрЕлки)
- огибание в пути медленных стрелок быстрыми
- взрывы (с разбрасыванием тел)
Короткая ремарка о стиле написания кода (для читателей первой части):
- не забывайте, код пишется а-ля псевдокод — не смотря на то, что он рабочий, во главу угла поставлена наглядность, а не функциональность и «правильность»
- расширяю базовый класс приложения (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
Заранее спасибо.
PPPS: добавил две строчки в код метода MainWaylines_2.explosion(): обнуляю векторы скоростей частицы перед взрывом — естественней смотрится
protected function explosion(e:MouseEvent):void
{
...
particleClone.velX = 0;
particleClone.velY = 0;
...
}