Портирование системы частиц Aurora, ч.3 - Архитектура
Наконец мы добрались до предмета обсуждения - самой системы частиц.
Честно, я не знаю, подсмотрел ли Лаури её архитектуру в каком-либо игровом движке (необязательно FPS), или это его оригинальная разработка.
В нашем случае это не так уж и важно, но на примере Aurora я хочу показать, как попытка сделать мощную и гибкую систему при недостатке опыта приводит к интересным последствиям.
Конечно, несмотря на все недостатки, эта система частиц всё равно стала неким стандартом для моддинга Half-Life, но как вы понимаете, это произошло на безрыбье. Альтернативы попросту не было. Не считать же таковой нативные пятнышки движка GoldSource, унаследованные ещё из первого Quake. Даже в 2001-м году это выглядело несерьезно.
Типичные способы применения системы частиц Aurora:
Ключевые особенности технической реализации:
- Каждая система должна быть прикреплена к объекту
- Каждая система описана в текстовом файле (из кода задавать нельзя)
- Система может быть либо включена, либо выключена, создать разовый всплеск частиц невозможно. То есть для выстрелов/попаданий пули и других аналогичных эффектов, создающих фиксированный набор частиц, система непригодна.
- Каждая частица в системе способна порождать новые частицы (тут честно признаюсь - такого я больше не видел нигде, обычно частицы порождает эмиттер).
Пример описания дыма, для лучшего понимания внутренних особенностей системы:
"particles" "100" "maintype" "smokesource" { "name" "smokesource" "spraytype" "smoke" "sprayrate" "10" } { "name" "smoke" "lifetime" "2" "gravity" "70" "sprite" "sprites/blacksmoke.spr" "startalpha" "1" "endalpha" "1" "startsize" "0" "endsize" "90" "startframe" "1" "endframe" "16" "startangle" "0..360" "angledelta" "40..-40" "startred" "0" "endred" "0" "startgreen" "0" "endgreen" "0" "startblue" "0" "endblue" "0" "rendermode" "solid" "windyaw" "45" "windstrength" "30" "drag" "0" }
Как видите, скрипт состоит из параметров вида "ключ" "значение" - либо помещённых в безымянный блок фигурных скобок, либо вне него.
Параметры вне блоков глобальные, а каждый блок скобок обозначает либо частицу-эмиттер, либо просто частицу, либо частицу-оверлей (к этому мы ещё вернёмся).
Параметр "particles" - это компенсация проблем внутренней реализации. Напрямую этот параметр на количество частиц не влияет, но задать ему слишком маленькое значение, то система будет рисовать недостаточное количество частиц. Если слишком большое - то выделит память, которую не будет использовать. Предполагается, что художник должен сам понимать сколько примерно частиц его система рисует, однако же никаких способов это узнать или как-то замерить в самой системе попросту нет. Т.е. у этого симулятора нет никакой статистики. Здесь мы потенциально можем получить проблему перерасхода оперативной памяти, когда человек, создающий эти скрипты, будет ставить заведомо большое число, вроде 32768, "чтобы уж наверняка хватило". В начале нулевых это было критично.
Следующий параметр - "maintype". Тут начинается самое интересное. Первое, что хотелось бы отметить - система не может иметь несколько независимых друг от друга эмиттеров. Источник всегда один, точку рождения частицы рандомизировать нельзя, она фиксирована и жестко определена как позиция объекта, к которому прикреплён источник. Вариантов изменить это поведение не предусмотрено, как я уже упоминал. Но это ещё только половина проблемы.
В конце концов, раз уж жесткая линковка с игровым объектом является важной особенностью системы, то стоит подумать, где такое поведение может быть полезным?
К примеру, можно сделать шлейф дыма от ракеты - но не всё так просто. Дело в том, что направление и скорость полёта каждой порождённой частицы... жестко заданы всё в том же скриптовом файле и не могут быть изменены. Иными словами, куда бы не летела ракета, шлейф дыма от нее всегда будет направлен вверх (если так прописано в файле скрипта).
Конечно настройки позволяют указывать не только фиксированные значения, но и диапазон, в котором выбирается случайное значение в момент рождения частицы. Это хорошо и удобно, но здесь так и напрашивается потребность трансформировать направление полёта частицы, заданное в скрипте, в локальном пространстве самого объекта. И у меня есть только одно объяснение, почему это не было реализовано - автор системы попросту не знал как это сделать.
На этом примере мы видим как отсутствие определённых знаний у разработчика впоследствии оборачивается проблемами у пользователей вплоть до потери желания использовать систему вообще.
Также, хочу развеять один вредный стереотип - будто пользователи, увидев в предлагаемой системе недостатки, начнут оперативно писать об этом автору, предлагая ему способы улучшения и исправления.
Ничего подобного не произойдет! Люди могут похвалить автора, чтобы сделать ему приятно, но пользоваться его системой (из-за тех или иных недостатков) попросту не будут и никому ничего не скажут.
Поэтому опытный разработчик всегда должен ставить себя на место пользователя, который остается один на один с новым продуктом и помощи ему ждать неоткуда (т.к. продукт новый, базы знаний по нему ещё накоплено).
Однако мы отвлеклись, вернёмся к изучению параметров скрипта.
Далее у нас идёт безымянный блок фигурных скобок. Возможно автор вдохновлялся описанием объектов в bsp-формате, где их параметры точно так же находятся в безымянном блоке, но здесь такой подход не слишком удобен. Следовало учитывать контекст, ведь тот формат описания создавался для скриптового языка QuakeC, где все настройки равнозначны с точки зрения виртуальной машины.
Имя блока "name" логичнее вынести за скобки - читабельность только улучшится и сразу станет понятно, что описывает в блоке, учитывая что есть и настройки, значения которых ссылаются на другие блоки.
{ "name" "smokesource" "spraytype" "smoke" // ссылается на блок "smoke" "sprayrate" "10" }
"smokesource" { "spraytype" "smoke" "sprayrate" "10" }
Затем идут параметры "spraytype" и "sprayrate". Есть ещё "sprayyaw", "spraypitch" и "sprayforce". Обратите внимание, что в данной секции не задается никаких видимых объектов, только параметры эмиттера. С точки зрения кода - это точно такая же частица, как и прочие, но благодаря тому, что мы указали её имя в "maintype", она считается главным (и единственным) эмиттером. Лаури пытался передать главному эмиттеру ускорение от объекта для частичной компенсации отсутствия нормальной трансформации углов в пространстве объекта, о котором я упоминал выше, но по правде говоря, положение это не исправило, а наоборот только ухудшило. Посудите сами - ведь если ракета летит вперёд, то дымный шлейф должен быть направлен назад. А в случае вращения объекта, его ускорение либо не меняется, либо частицы рандомно летят во все стороны. С такой же "компенсацией" дым начинал лететь вперёд ракеты...
Небольшое лирическое отступление:
Во времена создания Xash-мода под собственный движок я встроил туда полноценную систему иерархических зависимостей (movewith, parent system). Само собой, мне захотелось протестировать её в сочетании с системой Aurora (изначально добавленной в мод без изменений). Частицы летели куда угодно, но только не туда, куда нужно.
Я внёс в систему ряд исправлений, научил её прикрепляться не только к позиции объекта, но и к аттачментам на моделях, исправил все трансформации и Aurora стала гораздо дружелюбнее к пользователю.
Итак, повторюсь, у нас в системе есть некая невидимая частица, которая в свою очередь порождает другие частицы, описанные очередным безымянным блоком фигурных скобок.
Имя этого блока (параметр "name") должно совпадать с именем, указанным в параметре "spraytype". Указывать несколько эмиттеров не имеет смысла - "maintype" может быть только один. Аналогично, и сам эмиттер может порождать лишь один тип частиц. При рождении, частице придается ускорение, указанное в "sprayforce", которое можно и не указывать, если вы предполагаете, что частица будет лететь либо вверх, либо вниз. В таком случае, в настройках частицы необходимо указать параметр "gravity". Направление полёта задаётся параметрами "sprayyaw" и "spraypitch", которые могут быть заданы диапазоном. Значением "0..360" можно указать полный охват угла yaw. Так можно сделать, например фонтанчик. Однако для систем, имеющих ярковыраженную направленность, подобный способ задания угла крайне неудобен.
Идём дальше, "sprayrate" - это количество порождаемых в секунду частиц. Казалось бы, идеальный кейс - связать вместе параметр "particles" с этим. Но нет, система изначально задумывалась как чрезвычайно гибкая, поэтому автор рассудил, что точно рассчитать количество активных частиц не получится. На практике эта гибкость пригодилась для реализации всего-навсего двух конструкций.
Параметр "sprayforce" отвечает за первоначальное ускорение вновь рождённой частицы, применённого в направлении, заданном при помощи углов "sprayyaw" и "spraypitch". В принципе это нормальная практика - задать начальную скорость один раз, а во время жизни частицы лишь слегка её корректировать. Однако параметр ускорения, заданный в описании частицы, действует только на движение вверх-вниз и представлен неявным образом - через имя "gravity".
Есть дополнительные параметры "windyaw", "windforce" и "drag", которые позволяют отклонять частицу по горизонтали, но их названия, на мой взгляд, также неудачны. К тому же без параметра "drag", настройки "ветра" работать вообще не будут.
Не припоминаю, было ли это отражено в документации, но я видел достаточно .aur-файлов, где параметры действия ветра прописаны, а "drag" равен нулю. То ли "ветер" не давал ожидаемый эффект и его отключили, то ли настройки оказались слишком сложными для понимания.
Остальные параметры намного проще и интуитивно понятны - цвет, размер, прозрачность, угол поворота, кадр анимации при рождении и, соответственно, в момент гибели. Данный набор настроек характерен для большинства систем частиц и каких-то особенных сюрпризов здесь нет.
Выделяется лишь параметр "overlaytype" - подобно параметру "spraytype", он также порождает новые частицы, но их кол-во всегда ограничено одной (т.е. каждая частица породит ещё одну). Большинство параметров дочерних частиц (такие как позиция, ускорение, угол поворота и текущий размер) берутся из родительской. Из своего у них лишь собственный цвета, текстура и режим смешивания прозрачности. Потенциально этих оверлеев может быть сколько угодно, однако из-за ограниченного функционала на практике обходились одним слоем. Возможно, так пытались решить проблему с выводом полупрозрачных объектов в правильном порядке, не прибегая к их сортировке в каждом кадре. В те времена это стоило довольно дорого. В то же время, ограничение параметра "overlaytype" единицей, делавшееся для древнего OpenGL без шейдеров, снизило гибкость системы в целом.
Так же стоит отметить, что каждая из частиц может не только иметь оверлей, но и быть одновременно эмиттером для других частиц. Однако тонкости этих иерархических построений оказались слишком сложны для большинства пользователей. К тому же сыграл роль любопытный эффект, когда система, неспособная нормально учитывать углы от родительского объекта, порождает частицами новые частицы, которые так же могут иметь несколько оверлеев. Вероятно об этой возможности мало кто знает.
Практически все файлы Aurora, которые я видел, состояли из двух секций (источник, описание частицы), либо из трёх (источник, описание частицы, оверлей частицы). Полагаю что частицы, бесконтрольно порождающие частицы, на практике не особо нужны. Пригодилось бы рождение по событию, например в момент коллизии с поверхностью. Тогда средствами системы можно было бы создать нечто вроде эффекта взрыва кластерной гранаты. Однако такой функционал предусмотрен не был и его внедрение окончательно запутало бы пользователей.
Хотя как знать, определяющую роль играет не столько сложность, сколько наличие обучающих материалов по тому или иному предмету. Но, как вы понимаете, ничего кроме документации от автора на тот момент не существовало. Тогда даже Ютуб не существовал, а от первого видеоурока на нём нас отделял добрый десяток лет.
Тем не менее, мы с удовольствием использовали систему частиц Aurora не выходя за рамки, неявно установленные её автором в созданных им примерах. И у каждого из нас было ощущение, что эта невероятно мощная система. Тогда не было ни времени ни навыков подробно в ней разобраться и проанализировать. Что я, собственно, и сделал в этом цикле статей теперь.
Итого
Задуманный функционал, зачастую, либо не был реализован должным образом, либо оказался невостребован самими пользователями. Тем не менее, в отсутствии аналогов, системе де-факто удалось стать стандартом в среде моддинга Half-Life.
Можно было бы также рассказать о конвертации скриптов Авроры в формат, понятный нативной системе частиц XashNT. Однако особых сложностей это не вызвало, а цикл статей и так получился достаточно объемным.
Поэтому, при наличии интереса, сообщите об этом, пожалуйста, в комментариях.
© Seamless Realm www.seamless-realm.ru