Войти
ПрограммированиеСтатьиГрафика

Создание гибкой системы частиц на основе стратегий.

Автор:

Данный материал является адаптированным переводом статьи “Designing an Extensible Particle System using C++ and Templates, написанная Kent “_dot_” Lai”. Мне показалось, что автор вышеназванного ресурса немного непоследовательно рассказал о предмете, да и приведенные примеры достаточно абстрактные. Поэтому моим решением было на конкретной задаче показать преимущество подхода, и немного упорядочить изложение. Итак, приступим.

Теория и не только
Реализация
Часть 2

Теория и не только

Огонь, дождь, взрывы, даже звезды. Все эти эффекты зачастую реализуются в виде системы частиц. Так как явления разные по своей природе, то хотелось бы создать такую систему, в которой каждый новый эффект можно было создать изменив несколько параметров и легко интегрировав код в существующую реализацию. Данная статья попробует высветить один интересный подход к проектированию легко расширяемой и гибкой системы частиц.

Как все наверное знают, существует несколько подходов к реализации такого типа комплексов.

Первый это конечно же наследование.  Создав абстрактный класс, мы создаем его потомков, переопределяя виртуальные функции, тем самым реализуя нужное нам поведение. Но использование таблицы виртуальных вызовов, при тысячах обрабатываемых частицах, может серьезно повлиять на общую производительность.

Возможно создание отдельных классов систем частиц, которые интегрируются в менеджер системы. По производительности этот метод лучше предыдущего, но скорость и удобство разработки страдает, ведь для каждой новой системы надо создавать управленческий механизм и механизм взаимодействия частиц сначала.

О третьем подходе пойдет речь в данной статье. Этот подход реализует систему частиц на основе шаблона проектирования, известного как «стратегия».

Для тех кто не знаком с понятием немного пояснений. Метод основывается на использовании шаблонных (template) классов, которые описывают поведение объекта. Данные классы предстают как шаблонные параметры для главного класса-менеджера, который, используя их, производит инициализацию и обработку всех частиц системы. Приношу свои извинения, если объяснил немного не понятно, надеюсь в процессе чтения вопросы исчезнут. Да, для тех кто больше хочет узнать о данном шаблоне (и о множестве других) есть замечательная книга «Современное проектирование на С++ (Modern C++ Design)» Андрея Александреску. Прочитайте, не пожалеете.

Реализация

Итак, хватит слов пора заняться программированием. Давайте сначала попробуем описать общие принципы дизайна нашей системы частиц.

Перво-наперво определим будет ли содержать наша система методы рендеринга? Можно было бы, но это уменьшит гибкость нашей системы, так как область применения будет ограничена одним АПИ. Я считаю, что достаточно удобно хранить лишь 2 параметра частицы которые можно использовать позже рендером для отрисовки: это позиция (x,y,z) и размер частицы (Size). В вашей реализации Вы можете интерпретировать эти значения как хотите. Можете рисовать квадратный билбоард с размерами Size x Size, или треугольник со сторонами Size, или даже объемный осколок. Это ваше право.

Далее определимся с методом хранения партиклов в памяти. Тут, опять же, несколько вариантов. Можно использовать динамическое выделение памяти, или, например, std::vector. Все эти подходы имеют право на жизнь, но мне кажется что они неоправданно усложнят программу, а передать количество частиц в систему мы можем с помощью шаблонного параметра. Ну, вроде бы все.

В отличие от автора вышеупомянутой статьи, я избрал путь снизу вверх, объясняя сначала основу системы частиц постепенно переходя к обобщению в законченный комплекс. Так мне кажется будет более понятно, и менее запутанно.
Реализовывать начнем с описания структуры отдельной частицы системы. Никаких методов содержать она не будет, так как вся обработка данных будет производится нашими стратегиями.

typedef struct
{
  remVector3f    Pos;
  remVector3f    Velocity;
  float      Life;
  remColor4f    Color;
  remVector3f    SizeP;
  remVector3f    Grav;
} CommonParticle;

Тут должно быть все понятно. Pos – позиция частицы, Velocity – скорость, Life – жизнь (можно использовать для вычисления альфа значения цвета, для того чтобы частицы постепенно пропадали), Color – цвет, SizeP – размер, Grav – гравитация (для эффекта силы притяжения или ветра). Кстати настоятельно рекомендую обзавестись хорошей математической библиотекой, которая реализует классы Vector3, Color3, Color4 и т.д. Это здорово облегчит обработку данных в стратегиях.

Теперь начинается самое интересное. Наши стратегии. Я обещал конкретику - получите. Попробуем разработать 2 типа системы частиц: дымок и взрыв. Надеюсь конкретная реализация поможет четче понять мощь и гибкость применения стратегий.

Как и в обычной системе частиц, сначала частицы надо проинициализировать, а затем обрабатывать. Поэтому имеем 2 подтипа стратегий: инициализаторы и обработчики. Так как параметров у нашей частицы 6, то, соответственно, имеем 12 типов стратегий: инициализаторы позиции, жизни, скорости, гравитации, цвета и размера, а также соответствующие обработчики. Система у нас гибкая, следовательно все стратегии совсем не обязательно описывать, если конкретный инициализатор или обработчик не требуется. Как это реализуется в программе, смотрите ниже.

Начнем, пожалуй, с инициализаторов. Для наших тестовых систем гравитацию инициализировать не будем, а позицию зададим явно при создании эффекта, так что инициализатор позиции не требуется. Цвет будет задаваться произвольно для каждой частицы.

// Инициализация цвета. Произвольный.
struct remInitColor
{
  inline void operator () (CommonParticle *P, double &FrameTime)
  {
    P->Color = remColor4f(1.0f/(float)(rand()%10), 1.0f/(float)(rand()%10), 
                       1.0f/(float)(rand()%10), 0.7f);
  }
}; 

// Инициализация размера
struct remInitSize
{
  inline void operator () (CommonParticle *P, double &FrameTime)
  {
    P->SizeP = remVector3f(0.2f, 0.2f, 0.0f);
  }
};

// Инициализация жизни.1000
struct remInitLife_Smoke
{
  inline void operator () (CommonParticle *P, double &FrameTime)
  {
    P->Life = 1000;
  }
};

// Инициализация скорости для взрыва. Во все стороны.
struct remInitVel_Explosion
{
  inline void operator () (CommonParticle *P, double &FrameTime)
  {
    P->Velocity = remVector3f(10 - (float)(rand()%200)/10, 
                  10 - (float)(rand()%200)/10, 
                  10 - (float)(rand()%200)/10);
  }
};

// Инициализация скорости для дыма. Вверх.
struct remInitVel_Smoke
{
  inline void operator () (CommonParticle *P, double &FrameTime)
  {
    P->Velocity = remVector3f(0.0f, 2.0f, 0.0f);
  }
};

Для тех кто еще не познал все глубины языка С++, не пугайтесь, struct это тот же класс только в нем все члены по умолчанию объявлены как public.

Как видите, реализация каждой стратегии небольшая. Есть одно важное условие: каждая стратегия должна реализовывать одну «главную» функцию, которая будет вызываться менеджером системы частиц для каждой частицы. Что это будет за функция - без разницы, главное чтобы она была одинаковая во всех стратегиях. Естественно, другие функции в стратегии, которые будут использоваться «главной» функцией, можно добавлять в каком угодно количестве и какие угодно. Удобной является функция оператор (). Красиво, и писанины меньше. В данном случае давайте условимся, что у нашей «главной» функции должно быть 2 параметра: указатель на частицу, с которой работаем, и время таймера для отвязки динамических параметров от производительности компьютера.

Теперь рассмотрим реализацию стратегий-обработчиков.

// Обработчик позиции дыма. Добавляем немного тряски в горизонтальной 
// плоскости
struct remProcPos_Noise
{
  inline void operator () (CommonParticle *P, double &FrameTime)
  {
    remVector3f g((float)(5-rand()%10), 
            0.0f, 
            0.0f);
    g += P->Velocity;
    g.Vector.x *= FrameTime;
    g.Vector.y *= FrameTime;
    g.Vector.z *= FrameTime;
    P->Pos = P->Pos +  g;
  }
};

// Просто линейное приращение скорости к текущей позиции
// Для взрыва вполне подходит
struct remProcPos_Linear
{
  inline void operator () (CommonParticle *P, double &FrameTime)
  {
    remVector3f g = P->Velocity;
    g.Vector.x *= FrameTime;
    g.Vector.y *= FrameTime;
    g.Vector.z *= FrameTime;
    P->Pos = P->Pos + g;
  }
};

// И не забыть уменьшить жизнь частицам.
struct remProcLife
{
  inline void operator () (CommonParticle *P, double &FrameTime)
  {
    P->Life = P->Life - 500.0 * FrameTime;
    P->Color.RGB.a = (float)P->Life/1000.0f;
  }
};

Страницы: 1 2 Следующая »

#particle system, #стратегии

13 сентября 2003 (Обновление: 15 июня 2009)

Комментарии [25]