многие идеи, которые приходят ко мне, уже кто-то реализовал или скоро реализует (цитата с просторов интернета)
В далеком 2001 году меня, любителя стратегий реального времени, поразила игра “Казаки”. Поразила ГИГАНТСКИМИ толпами, бродящими по карте. Поразило то, что эти толпы довольно резво бегали на тогдашних маломощных компьютерах. Но в то время я работал на скорой помощи, был далек от программирования, потому восхищением дело это тогда и ограничилось.
Уже в наше время захотелось сделать игрушку с примерно подобным количеством подвижных юнитов — чтоб “эпик” просто зашкаливал(!). И чтоб эти юниты не просто двигались, а двигались внешне(!) осмысленно. И чтоб (главное), все это великолепие работало на слабеньких мобильных платформах.
Встал вопрос — как? С графикой вопросов нет — на любой современной платформе есть разные по-произодительности графические библиотеки, которые займутся выводом толпы на экраны. Главный вопрос — как программно реализовать осмысленное (ну или бессмысленное) нечто, что игроком воспринималось бы однозначно — это подчиняющаяся каким-то стимулам толпа, а не просто набор мельтешащих фигурок.
Уверен, существует куча рекомендаций, литературы, и даже реализаций. Но меня интересовало что-то “простенькое”, что можно применить в незатейливой игрушке для “мобилы” и собрать “на коленке”. Т.е. дешево и сердито, а главное — понятно для меня(!).
Год назад прослушал в записи замечательную лекцию с КРИ 2008 Михаила Блаженова (это АУДИО — печатной версии не нашел) “Специфика конвейерной разработки мобильных игр: планирование, портирование, тестирование”. Важный вывод из статьи для меня — при написании логики приложения с большим количеством данных, надо использовать подход, ориентированный на данные (data-oriented paradigm). (Тадааам!).
Суть, родившейся после прослушивания, идеи была в следущем.
- Все (ВСЕ) разнообразные “побудительные мотивы индивида” в игре надо описывать системой векторов (приказ, страх, ненависть, лень, глухота, инерция…). В течении одного игрового цикла (или каждого десятого, не суть) проводятся различные манипуляции с этими векторами. Итогом всех манипуляций будет сумма векторов, которая в простейшем случае определит направление движения персонажа (одного из сотен, а то и тысяч).
- Каждый “индивид” должен обладать одинаковым набором векторов — ради стандартизации вычислений. Тогда определением поведения отдельного “индивида” в толпе может заниматься маленький многократно повторяющийся кусочек кода — конвейер. Прелесть конвейера в том, что ему абсолютно все равно “кого” он обрабатывает — толстяка, катящееся колесо, труп… Ему главное — набор “мотивов”. Благодаря этому мы на выходе получаем не просто толпу, а разнообразную(!) толпу.
Теперь надо было писать сам механизм… Но какого черта?! Ведь если тебя посетила идея, то велика вероятность, что она уже посетила еще сто-миллион человек до тебя. А может кто-то из этого миллиона даже подходящий инструмент создал? Библиотеку какую-нибудь?
Такой инструмент нашелся — система частиц (particles system). Я выбрал FLINT.
- системе частиц идеально подходит для прототипирования
- расчеты внутри систмы частиц строятся на законах классической механики — т.е. в наше время уже существует несчислимое множество разнообразных алгоритмов, которые можно адаптировать под собственные программистские нужды
- к системе частиц можно прикрутить любой рендер из сторонней библиотеки
- данный варинт системы частиц написан автором Entity System фреймворка Ash by Richard Lord (это отдельная песня).
Кратко опишу базовые инструменты конкретной реализации системы частиц:
- эмиттер (Emitter2D) — собственно экземпляр класса, который отвечает за генерацию частиц, согласно заданным параметрам, и дальнейшее “сопровождение”
- Emitter2D.addInitializer( initializer ) — метод, при помощи которого в эмиттер добавлются “свойства”, которые будут присвоены частицам при их генерации.
- Emitter2D.addAction( action ) — при помощи этого метода в эмиттер добавляются правила управления частицами, которые будут применяться к частицам во время их жизни.
Конечно, в мечтах, мне кажется, что с помощью “векторов мотиваций” можно даже сделать РПГ с кучей “независимых”, со своим мнением персонажей в отряде, но начинать надо с простого. Я выбрал жанр Tower Defence. Только в игре хочу увидеть не жиденький ручеек “танчиков”, а сонмы вражин(!), чтоб их можно было, как щебенку, раскидывать взрывами, крошить, плющить, рассеивать…
Уфф… конец предисловия. Но мне кажется, оно важно.
Реализация идеи
Начнем с простейшей задачи:
- генерировать маршрут любой сложности
- персонажи следуют по заданному маршруту, при этом
- не “налезают” друг на друга
- “смотрят” по направлению движения
1. генерировать маршрут любой сложности
Т.е. — путь состоит из точек (waypoints). Частицы должны двигаться последовательно от точки к точке. К сожалению, соответствующиего action в библиотеке не оказалось. Но я подсмотрел реализацию в другой библиотеке (которая, кстати писалась на основании выбранной мной) — stardust-particle-engine.

Единствоенное, в найденной реализации мне не нравится, что в точках пути частицы сбиваются в кучу, а не следуют “широким фронтом” вдоль всего маршрута. Поэтому я чуть модифицировал класс под свои нужды
Wayline — имя класса, неуклюжее “наследование” от оригинального Waypoint
суть класса — хранить перпендикуляр к касательной пути. При генерации частицы, с помощью action FollowWaylines, сохраняют данные о своем относительном положении на перпендикуляре. И далее во время движения “стараются” этого положения придерживаться — таким образом они не сбиваются в кучу в узлах пути — они распределяются по-перпендикуляру к касательной в этой точке (кстати, за это отвечает экземпляр класса Line, из замечательнейше-полезной библиотеки для кривых Безье).
package waylines.waypoints { import flash.geom.Point; import flash.geom.Line; public class Wayline extends Waypoint { public var rotation:Number; public var line:Line; public function Wayline(x : Number = 0, y : Number = 0, segmentLength : Number = 40, rotation:Number=0, strength : Number = 1, attenuationPower : Number = 0, epsilon : Number = 1) { super(x, y, segmentLength/2, strength, attenuationPower, epsilon); this.rotation = rotation; this.line = new Line(new Point(x - (radius * Math.cos(rotation)), y - (radius * Math.sin(rotation))), new Point(x + (radius * Math.cos(rotation)), y+(radius * Math.sin(rotation)))); } } }
затем генерируем путь, состоящий из узловых точек
protected function setupWaylines():void { _waylines = []; var w:Number = stage.stageWidth; var h:Number = stage.stageHeight; /* * 1. это я на глаз накидал координаты, просто доли умножая на ширину и высоту экрана * 2. первую и последнюю точку расположил за пределами экрана, чтоб частицы появлялись из-за края экрана, а не из пустоты */ //var points:Array = [new Point(-9,h*.6), new Point(w*.3,h*.3), new Point(w*.5,h*.25), new Point(w*.6,h*.45), new Point(w*.7,h*.7), new Point(w*.8, h*.75), new Point(w*.9, h*.6), new Point(w*1.3, h*.5)]; 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)]; /* * проблемы: * 1. экземпляры Wayline должны быть выставлены перпендикулярно к касательной пути * 2. количества первоначальных точек маловато для обеспечения плавности пути * 3. лень вручную выставлять все необходимые параметры * решение: * взять нужный кусочек из полезнейшей библиотеки http://silin.su/#AS3, где * 1. FitLine - класс, который "сглаживает" ломаные кривые * 2. Path - класс, сохраняющий последовательность кривых Безье, которыми можно оперировать, как единым целым. Например - найти точку на пути. * 3. "нарезать" получившийся сглаженный путь на нужное число точек */ var fitline:FitLine = new FitLine(points); var path:Path = new Path(fitline.fitPoints); /* * ВАЖНО! - величине шага желательно должно быть больше скорости движния частицы - это ради упрощения расчетов. * Иначе, частица на скорости может "проскочить" следующую точку на пути, и "захочет" вернуться, или не впишется в резкий поворот. * В общем - будет выглядеть так, будто частица слетает с рельсов, или круги наматывает... Можете поэкспериментировать */ var step:Number = path.length / 40; /* * сила притяжения точки на пути - помните, система частиц работает "на классической механике"? * т.е. внутри системы частиц, частицу последовательно ускоряет к следующему узлу пути - как в адронном коллайдере */ 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); } }
2. персонажи следуют заданному маршруту, при этом
- не “налезают” друг на друга
- “смотрят” по направлению движения
этот пункт реализуется при помощи настроек самой системы частиц. Т.е. — настроим эмиттер
protected function setupEmitter():void { // --- создаем экземпляр класса и задаем параметры, которые которые будут применяться к частицам при их генерации ------------- var emitter:Emitter2D = new Emitter2D(); // это счетчик - генерирует определенное количество частиц в секунду emitter.counter = new Steady(60); // берем начало пути var wayline:Wayline = _waylines[0]; // позиционируем зону генерации эмиттера LineZone таким образом, чтоб она совпадала с линией "внутри" Wayline 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( ArrowBitmap, [4] ) ); emitter.addInitializer( new ImageClass( Arrow, [4] ) ); // --- добавляем actions, которые в совокупности будут определять поведение частиц --------------------------------------------- // определяем зону вне которой частицы будут автоматически(!) уничтожаться. Т.е. области организации движения составляют своеобразную матрёшку // 1. снаружи (больше всех) - мёртвая зона - вне прямоугольника частицы автоматически уничтожаются // 2. посредине - узлы маршрута частиц (вне экрана устройства концы, при этом внутри "прямоугольника жизни") // 3. внутри матрёшки - экран устройства - с одной стороны частицы входят, с другой - выходят // на самом деле этот action можно не добавлять, потому что уничтожение частиц в конце пути осуществляет "главный" action FollowWaylines emitter.addAction( new DeathZone( new RectangleZone( -30, -30, stage.stageWidth+60, stage.stageHeight + 60 ), true ) ); // new Move() - дает возможность обновлять позицию частицы каждый фрейм. Т.е. - чтоб частица двигалась, к эмиттеру надо прицепить этот action emitter.addAction( new Move() ); // это, чтоб персонажи были повернуты по направлению к движению emitter.addAction( new RotateToDirection() ); // определяет МИНИМАЛЬНУЮ дистанцию между частицами emitter.addAction( new MinimumDistance( 7, 600 ) ); // пришлось написать специальный action для ограничения скорости (есть и "родной" SpeedLimit, но он меня не устраивает - об этом в следующей статье расскажу) emitter.addAction( new ActionResistance(.4)); // наш "доморощенный" action, который непосредственно и контролирует движение частицы по заданному маршруту emitter.addAction( new FollowWaylines(_waylines) ); // создаем рендерер //var renderer:BitmapRenderer = new BitmapRenderer(new Rectangle(0, 0, stage.stageWidth, stage.stageHeight)); var renderer:DisplayObjectRenderer = new DisplayObjectRenderer(); // цепляем его на сцену addChild( renderer ); // передаем в качестве параметра настроенный эмиттер renderer.addEmitter( emitter ); // командуем старт emitterWaylines = emitter; emitterWaylines.start(); }

Итак, в результате поиска необходимых для нашей задачи библиотек и минимальнейшего “допиливания”, получился вполне приемлемый результат при хорошем соотношении времязатраты-эффективность (и даже, не побоюсь этого слова, эффектность!). И это только прототип, запущенный в дебаг-плеере.
При релизе можно (даже нужно) оптимизировать код:
- объединить некоторые actions (например DeathZone и FollowWaylines, а также Move и RotateToDirection и ActionResistance). Т.е. — оптимизируя один action, мы таким образом уменьшаем число итераций минимум на число частиц в эмиттере.
- на прямых участках пути убрать промежуточные точки маршрута
Код доступен на google code.
PS: В следующей части хочу усложнить задачу. Добавлю:
- взрывы (с разбрасыванием тел)
- медленные персонажи (это будут крупные стрЕлки)
- огибание в пути медленных стрелок быстрыми
PPS: В еще какой-нибудь статье портирую код на яваскрипт (чуть опыта поднаберусь, чтоб времени не убивать много)
ссылка на оригинал статьи http://habrahabr.ru/post/218473/
Добавить комментарий