Урок по Maya от Дмитрия Ивановича

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

Конечно, скрипт, который я приведу, не претендует на 100% полноту, краткость и ясность. Я не намерен серьезно модифицировать или дополнять его, т.к. не буду распространять в виде отдельного инструмента, хотя у меня осталось несколько идей по поводу его улучшения. По ходу урока, я изложу некоторые из них. Вы вольны изменять скрипт по своему усмотрению, добавлять и удалять что заблагорассудится, однако мое имя в списке авторов будет смотреться как нельзя кстати. Скачать скрипт в удобочитаемом виде, а также файлы, которые я использовал при написании урока, можно здесь:

Вступление

К любому делу негоже подходить неподготовленным. Особенно к такому важному событию в жизни любого мужчины, как написание собственного MEL-скрипта. Для осуществления задуманного, давайте сначала рассмотрим некоторые важные, по моему мнению детали.

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

Поверхность земли вы можете подготовить в любой удобной для этого дела программе – будь то Bryce, ZBrush или что-либо другое. Я же создал polygonal plane размерами 2000х2000 и использовал стандартный Маевский Sculpt Geometry tool, чтобы придать ландшафту неоднородность. Не забудьте назвать Ваш ландшафт "terrain". Это необходимо, чтобы скрипт "узнал", где находится поверхность земли.

Подготавливаем ландшафт.

Из чего же сделаны наши десантники?

Модель юнита, в принципе может быть любой, но для экономии времени и собственных нервов не стоит использовать слишком уж детализированные модели. 1-2 тысячи полигонов на юнита вполне достаточно.
1 Анимация. Создание кэшей

Впрочем, остается один вопрос – как мы будем анимировать наших ребят? Системы костей, всевозможные deformers и, упаси Бог, FBIK отпадают. Реализовать из скрипта управление над каждой костью в каждом кадре – задача для обладателя мэйнфрейма и нескольких лет свободного времени. Но выход есть - геометрический кэш.

Наверняка многие сталкивались с понятием кэш при работе с динамикой, частицами, одеждой и т.д. Там кэширование, т.е. запись динамической симуляции в файл, используется из-за большого объема вычислений в каждом кадре, что тормозит проигрывание. Но для меня стало приятной неожиданностью, когда я узнал, что точно так же можно сохранить и любую анимацию. Чрезвычайно удобно, то, что за анимацию десантника отвечает лишь несколько нод – ими, в отличие сложной иерархии костей, очень легко управлять, когда количество солдат в сцене приближается к сотне. Команды, посвященные Geometry Caching, обитают в Animate > Geometry Caching. После того, как команда Create New Cache запишет выбранный вами промежуток анимации объекта в файл (которых на самом деле не один, а два - .mc и .xml), вы можете применить кэш к неанимированной модели, с которой вы снимали кэш. Используйте для этого команду Import Cache… Нужно помнить, что в геометрический кэш записываются конкретные позиции  всех точек объекта в каждом кадре, и поэтому не вздумайте спутать файлы кэшей и сами модели, иначе выйдет… в общем, ничего хорошего не выйдет. По сути – наш подход к анимации юнитов практически полностью совпадает с системой анимации в компьютерных стратегиях.

Не стоит назначать моделям «не родной» геометрический кэш.

Для наших опытов понадобится несколько анимационных последовательностей - fire1.mc(.xml) - геометрический кэш анимации атаки, idle1.mc(.xml) - кэш бездействия, run1.mc(.xml) - кэш бега, die1.mc(.xml) - кэш смерти. Единица в названиях означает лишь то, что можно сделать куда как больше различных вариантов анимаций. Впрочем, автор глубоко убежден, что настоящие бойцы должны делать все одинаково – ходить в ногу, зевать по команде и даже умирать так, как предписано уставом. Поэтому в нашем примере мы обойдемся одним вариантом анимации на каждое действие пехотинца.

Сейчас самое время вытащить из своих запасов анимированную модельку и прокэшить ее. Я же, продолжу клонирование нашего морпеха, модель и анимации которого, я, честно говоря, позаимствовал из одной известной игры. Впрочем не спешите звонить в Relic - по окончании урока, я обязательно верну все на место. Не забывайте, что для наших будущих целей удобнее будет использовать персонажа, состоящего из одного «куска» геометрии. Используйте для этого команду Combine перед тем, как начать кэширование. Мне, например пришлось приаттачить к туловищу голову и нож бравого десантника.

Не забудьте сохранить в отдельную сцену и саму модель. Сцену назовите «ref.mb». Конечно, вы можете не идти у меня на поводу, а использовать собственные искрометные имена для файлов, однако, в этом случае не забудьте подредактировать скрипт. Все подготовленные файлы поместите в отдельную папку, которую не забудьте назвать именем своего персонажа (я надеюсь, у него уже есть имя?). Я, так, назвал ее "marine".
2 Reference’ы как основа гибкого рабочего процесса

Зачем нужно сохранять модель в отдельный файл? А затем, что скрипт будет размещать в нашей сцене не сами объекты, а их reference’ы. На это есть несколько причин. Во-первых, если мы захотим подправить модель юнита, то достаточно будет открыть файл ref.mb, и внести изменения. Тогда во всех сценах, где присутствовала reference-связь с ref.mb автоматически обновятся связанные объекты. Вторая причина носит имя Proxy. Импортированным как Reference объектам можно назначить соответствующий proxy-объект, который будет отображаться во вьюпорте вместо него. Таким образом, можно заменить высокополигональные модели на упрощенные и выиграть в производительности. Особенно хорошо то, что к reference-объектам можно применять любые модификаторы, да и вообще работать как с обычным полигональным объектом. Вот вольный пересказ Help’а, где указано какие действия можно совершать с reference-объектами:

    * Парентить с не-reference объектами.
    * Соединять и разрывать связи с нодами и атрибутами не-reference объектов в пределах сцены.
    * Присваивать атрибутам значения.
    * Присваивать динамические атрибуты reference-объектам.

Надо отметить, что при создании reference есть два варианта присвоения имен объектам: с использованием префикса и с использованием namespace. Namespace – это группировка нескольких объектов под одним именем. Используется, чтобы избежать конфликта имен объектов. Иерархия Namespace’ов в принципе похожа на структуру файлов. Так – Namespace’ы – это директории, а имя объекта – это имя файла, только разделяются они не слэшем, а двоеточием (:), например - Biosphere:Plants:Trees:Oak:Leaves:Leaf94. С Namespace’ми можно работать как с частью имени объекта, а можно как с иерархической цепочкой.

Вспомогательные приспособления

К модели мы привяжем эмиттер частиц и пару ограничителей – normalConstraint и geometryConstraint.

1 Симуляция ураганного огня

С помощью эмиттера мы будем симулировать нанесение повреждений другим юнитам. Эмиттер испускает частицы, которые символизируют пули. Даже если юнит – рукопашник - не беда. Ведь эти «пули» будут невидимыми. Главное, что Maya позволяет при столкновении частиц с каким-л объектом (в нашем случае с другими пехотинцами) вызывать процедуру, которая и решит дальнейшую судьбу раненых субъектов. Там, где есть эмиттер, очень скоро будут частицы. Очень много частиц. А раз есть частицы – грех их не настроить. Создадим ноду с описанием частиц, например использую команду «particle;» (без кавычек) и настроим их примерно так:

Все по умолчанию, кроме:

Время жизни частиц постоянно и равно 0,5 сек.

Можно еще настроить Render Attributes (но это по желанию).

Назовите созданную систему частиц, к примеру "trace".

Только что созданные частицы – это те самые невидимые «пули».

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

2 Привязываем юнита к земле

GeometryConstraint привязывает юнит к поверхности какого-л объекта. Применив geometryConstraint к объекту, являющемуся «землей» в нашей сцене, мы будем задавать только координаты X и Z юнита, а высота будет рассчитываться автоматически, в зависимости от искривленности поверхности.

NormalConstraint ориентирует модель десантника относительно нормалей поверхности, чтобы тот не передвигался "столбом".

Сама модель определяет положение морпеха в пространстве, т.к. все остальные его запчасти привязаны именно к ноде геометрии. Кроме того, на модель подвешены геометрические кэши, которые определяют анимацию юнита.

3 Дополнительные параметры

Как любой прожженный ветеран, наш герой в огне уже не горит, в воде не тонет и обладает набором дополнительных параметров:
(после запятой указан тип: long, byte – это целое число, double – с плавающей точкой, bool – логическое значение.)
team, long - Параметр "Команда".
speed, double - Параметр "Скорость".
rov, double - Параметр "Радиус обзора".
range, double - Параметр "Дальность атаки".
rot, double - Параметр "Угол движения по умолчанию".
static, bool - Параметр "Стоит ли на месте".
state, byte - Параметр "Состояние". Возможные варианты: 1-атакует, 2-бездействует, 3-бежит, 4-мертв.

Эти параметры потребуются при расчете взаимоотношений юнитов.

Ближе к телу

Как известно, все команды Maya полностью построены на встроенном языке программирования MEL. Стоит совершить какое-либо действие, как его аналог на MEL тут же появится в поле History Script Editor’а. Это очень удобно, т.к. для того, чтобы узнать как вызывается та или иная команда нет необходимости  лезть в Help и перерывать функции сотнями.

Напомню, если кто забыл - вот так вызывается на свет божий Script Editor.

Например команде Import Cache… соответствует "importCacheFile ("/../../myCache.mc", "xml");".

В скрипте мы будем использовать глобальные переменные и процедуры. Это значит, что они будут доступны до самого завершения текущего сеанса в Maya откуда угодно.

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

Назовем ту, что первая, к примеру, - placeUnits. Ту, что вторая – addUnit. А ту, что третья – startSym.

Набросаю примерный план этих трех процедур:

Объявляем addUnit {
Шаг 1. Импортируем модель юнита, как reference, не забывая старательно использовать namespace’ы;
Шаг 2. Навешиваем на него геометрические кэши из файлов fire1.mc(.xml), idle1, run1, die1;
Шаг 3. Добавляем дополнительные атрибуты, устанавливаем их начальные значения согласно аргументам функции;
Шаг 4. Создаем эмиттер, привязываем к юниту, настраиваем;
Шаг 5. Привяжем анимацию юнита, а также состояние эмиттера к атрибуту юнита .state (чтобы управлять состоянием юнита с помощью одной «кнопки»);
Шаг 6. Добавляем Constraint’ы;
Шаг 7. Возвращаем порядковый номер созданного юнита.
}

Объявляем placeUnits {
Шаг 1. Вызываем процедуру addUnit;
Шаг 2. Размещаем и ориентируем юнит, созданный в Шаге 1.
Шаг 3. Повторяем вышеописанное столько раз, сколько необходимо.
}

Объявляем startSym {
Шаг 1. Найдем угол, на который нужно повернуться юниту за текущий кадр. Угол будет зависеть от цели юнита.
Шаг 2. Поворачиваем юнит.
Шаг 3. Найдем расстояние по осям x и z, на которые должен сдвинуться юнит за кадр.
Шаг 4. Передвигаем юнит.
Шаг 5. Не забываем проставить кейфреймы.
Шаг 6. Повторяем вышеперечисленное для каждого юнита…
Шаг 7. …и для каждого кадра.
}

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

Кстати, нелишним будет выяснить, что мы подразумеваем под осями X, Y и Z. В Maya, как известно, оси X и Z отвечают за плоскость земли, а Y за «высоту» объекта.

Система координат Maya по умолчанию. ©Автодеск Майа Хелп

К сведению взращенных на CAD-пакетах -  чтобы поменять Y и Z местами, нужно зайти в Window > Settings/Preferences > Preferences. В списке слева клацнуть на Settings и в поле World Coordinate System выбрать Y или Z.

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

 
Листинг скрипта с комментариями

////////////////////////////////////////////////

////////////////////////////////////////////////

global int $last; // Объявляем глобальный счетчик юнитов.
global string $terr = "terrain"; // Задаем имя объекта, олицетворяющего землю.
global string $trace = "trace"; // Имя частиц, которые будут представлять собой невидимые пули.

//Создаем локатор. Он понадобится при симуляции движения юнитов.

global string $tmp_loc;
$tmp_loc = stringArrayToString ((`spaceLocator -p 0 0 0`), ""); // Не забываем конвертировать тип "stringArray", который возвращает команда создания локатора, в "string".

////////////////////////////////////////////////

//Процедура, которая включает/выключает обработку реакции юнитов на попадание частиц-"пуль" (юнит умирает).

global proc colEventOnOff ( int $state) {

/*

Аргументы:

$state - Указание процедуре:
         1 - включить,
         0 - выключить.

*/

global string $trace;

if ($state == 1)  //Включаем.

         // Добавляем collision event, который "убивает" юнит, в которого попала "пуля", запуская процедуру "col".

         event -die 1 -ct 0 -n col -pr col $trace;

/* Команда event [-флаги] ($particle)

Создает Collision event с заданными параметрами для частиц с именем $particle.
Интересующие нас флаги:

-die(dieAtCollision) – Значение: 1 или 0. Данный флаг означает, что частица, столкнувшись с объектом, умирает,
-ct(count) – Количество испускаемых при столкновении частиц,
-n(name) – Имя создаваемого event,
-pr(proc) – Имя процедуры, вызываемой при столкновении,
-d(delete) - Данный флаг удаляет Collision Event с заданным флагом –name именем.
*/

if ($state == 0)  //Выключаем.
         // Удаляем event.
         event -d -name col $trace;
} // End of "colEventOnOff" PROC
////////////////////////////////////////////////
// Процедура выделяет все эмиттеры, созданные процедурой addUnits. С помощью нее будет легче скрыть эмиттеры во вьюпорте, чтобы те не мешали просмотру.
global proc selectAllEmitters () {
global int $last;
int $j;
select -cl; //Очищаем выделение
for ($j=1; $j <= $last; $j++){ select -add ("emitter"+$j);} // Выделяем все объекты с именами типа emitterN, где N - числа от 1 до $last.
return;
}
////////////////////////////////////////////////
//Процедура конвертирует порядковый номер юнита в его имя в сцене.
global proc string convName (int $n) {
/*
Аргументы:
$n - Порядковый номер юнита в сцене.
Если импортируемой модели автоматически присваивается другое имя, измените выражение ниже.
*/
return ("u"+ $n + ":unit");
} // End of "convName" PROC
////////////////////////////////////////////////
//Процедура импортирует юнит и применяет к нему свойства и параметры.
//Большинство аргументов остаются неизменными, т.к. процедура вызывается из процедуры "placeUnits".
//Возвращает имя импортированного юнита, чтобы его можно было переместить и ориентировать.
global proc string addUnit (int $j, string $path, int $team, float $speed, int $N, float $rov, float $range, float $rot, float $static, string $emitter_attr, string $vis_bullets) {
/*
Аргументы - такие же как и в процедуре "placeUnit", за исключением нескольких, не относящихся непосредственно к настройкам юнита.
Эта процедура вызывается из процедуры "placeUnit" (см. ниже по тексту.).
*/
//В MEL необязательно объявлять локальные переменные в отдельном блоке, можно делать это непосредственно перед использованием.
//Однако чтобы использовать глобальные переменные, нужно объявлять их заранее и обязательно с ключевым словом global.
global int $last;
global string $trace, $terr;
string $name = "u"+$j; // Задаем префикс юнита.
string $tmppath = $path+"ref.mb"; // Путь до модели юнита.
file -r -type "mayaBinary" -gl -loadReferenceDepth "all" -shd "renderLayersByName" -namespace $name -options "v=0;p=17" $tmppath; // Импортируем модель как reference.
//Выделяем только что импортированный юнит и…
$name = convName ($j);
select $name;
//…добавляем атрибуты (Extra Attributes).
addAttr -ln team -at long -min 1; // Добавляем параметр "Команда".
addAttr -ln speed -at double -min 0; // Параметр "Скорость".
addAttr -ln rov -at double; // Параметр "Радиус обзора".
addAttr -ln range -at double; // Параметр "Дальность атаки".
addAttr -ln rot -at double; // Параметр "Угол движения по умолчанию".
addAttr -ln static -at bool; // Параметр "Стоит ли на месте".
addAttr -ln state -at byte -min 1 -max 4; // Параметр "Состояние". Возможные варианты: 1-атакует, 2-бездействует, 3-бежит, 4-мертв.
/* Команда addAttr ([-флаги])
Добавляет объекту дополнительные параметры (Extra Attributes).
Интересующие нас флаги:
-ln(long name) – Имя добавляемого параметра,
-at(attributeType) – Тип добавляемого атрибута. Мы будем использовать: long, byte – целое число, double – с плавающей точкой, bool – логическое значение,
-min(minValue) – Минимальное значение параметра,
-max(maxValue) – Максимальное значение.
*/
//Делаем атрибуты пригодными для установки ключей.
/* Команда setAttr ([-флаги] object.attribute $value)
Устанавливает значение атрибута object.attribute равным $value
Интересующие нас флаги:
-k(keyable) – Делает атрибут пригодным для установки ключей. Значение: true, false,
*/
setAttr -k true .team;
setAttr -k true .speed;
setAttr -k true .rov;
setAttr -k true .range;
setAttr -k true .rot;
setAttr -k true .static;
setAttr -k true .state;
//Устанавливаем начальные значения атрибутов, в соответствии с аргументами процедуры.
setAttr .team $team;
setAttr .speed $speed;
setAttr .rov $rov;
setAttr .range $range;
setAttr .rot $rot;
setAttr .static $static;
setAttr .state 2; // В начале анимации юнит будет находиться в состоянии покоя.
//Импортируем кэш-файлы для геометрии.
select $name;
importCacheFile (($path+"fire1.mc"), "xml");
select $name;
importCacheFile (($path+"idle1.mc"), "xml");
select $name;
importCacheFile (($path+"run1.mc"), "xml");
select $name;
importCacheFile (($path+"die1.mc"), "xml");
//Устанавливаем коэффициенты влияния кэш-файлов на модель по нулям.
setAttr ("cacheBlend"+$j+".idle1Cache"+$j) 0;
setAttr ("cacheBlend"+$j+".fire1Cache"+$j) 0;
setAttr ("cacheBlend"+$j+".run1Cache"+$j) 0;
setAttr ("cacheBlend"+$j+".die1Cache"+$j) 0;
// ЗаLOOPиваем геометрические кэши.
setAttr ("idle1Cache"+$j+".postCycle") 1000;
setAttr( "fire1Cache"+$j+".postCycle") 1000;
setAttr( "run1Cache"+$j+".postCycle") 1000;
// Добавляем рассинхронизацию анимации, чтобы юниты не гипнотизировали нас одновременными движениями.
setAttr(("idle1Cache"+$j+".startFrame"), (rand(-50,0)));
setAttr(("fire1Cache"+$j+".startFrame"), (rand(-50,0)));
setAttr(("run1Cache"+$j+".startFrame"), (rand(-50,0)));
//Для анимации смерти устанавливаем атрибут "hold", чтобы после смерти юнит застыл в последней позе.
setAttr( "die1Cache"+$j+".hold") 1000;
//Создаем эмиттер частиц согласно аргументу.
eval $emitter_attr;
//Добавим некоторую неоднородность в параметре rate, чтобы эмиттеры не испускали частицы одновременно
setAttr ("emitter"+$j+".rate") (getAttr("emitter"+$j+".rate")+rand(-0.2,0.2));
//Заставим эмиттер испускать нужные нам частицы.
connectDynamic -em ("emitter"+$j) $trace;
/* Команда connectDynamic ([-флаги] $object)
Присоединяет динамический объект $object к полю, эмиттеру или объекту для столкновения.
Флаги:
-f(fields) – Присоединить к полю. Значение: имя поля,
-em(emitters) - Присоединить к эмиттеру. Значение: имя эмиттера,
-c(collisions) - Присоединить к объекту для столкновения. Значение: имя этого объекта,
-d(delete) – Удалять связи, а не создавать.
*/
//Установим направление распространения частиц - вдоль оси Z, прочь от юнита.
setAttr ("emitter"+$j+".directionX") 0;
setAttr ("emitter"+$j+".directionY") 0;
setAttr ("emitter"+$j+".directionZ") 1;
//Привязываем эмиттер к модельке юнита.
parent  ("emitter"+$j) $name;
//Сделаем так, чтобы испускаемые частицы сталкивались с юнитом.
collision -r 1 -f 0 $name;
/* Команда collision ([-флаги] $object…)
Подготавливает объект $object к столкновению с динамическими объектами и применяет определенные свойства к ему.
Флаги:
-r(resilience) – Устанавливает для объекта значение упругости,
-f(friction)  - Устанавливает для объекта значение трения.
*/
connectDynamic -c $name $trace;
if ($vis_bullets != "") connectDynamic -c $name  $vis_bullets;
//Нужно привязать анимации юнита и состояние эмиттера к атрибуту юнита "state".
//Например если указать state=1, то будет включена анимация атаки и эмиттер начнет испускать частицы. В других случаях частицы испускаться не будут, а анимация будет соответствовать состоянию юнита.
//Экспрешены подойдут для этого как нельзя кстати.
//Формируем динамически текст экспрешена…
float $rate = getAttr("emitter"+$j+".rate");
string $expr="if ("+$name+".state == 1) {setAttr cacheBlend"+$j+".fire1Cache"+$j+" 1; setAttr  cacheBlend"+$j+".idle1Cache"+$j+" 0; setAttr cacheBlend"+$j+".run1Cache"+$j+" 0;setAttr  cacheBlend"+$j+".die1Cache"+$j+" 0;setAttr emitter"+$j+".rate "+$rate+";} if ("+$name+".state == 2){setAttr  cacheBlend"+$j+".fire1Cache"+$j+" 0; setAttr cacheBlend"+$j+".idle1Cache"+$j+" 1; setAttr  cacheBlend"+$j+".run1Cache"+$j+" 0; setAttr cacheBlend"+$j+".die1Cache"+$j+" 0;setAttr  emitter"+$j+".rate 0;} if ("+$name+".state == 3) { setAttr cacheBlend"+$j+".fire1Cache"+$j+" 0; setAttr cacheBlend"+$j+".idle1Cache"+$j+" 0; setAttr cacheBlend"+$j+".run1Cache"+$j+" 1; setAttr  cacheBlend"+$j+".die1Cache"+$j+" 0; setAttr emitter"+$j+".rate 0;}if ("+$name+".state == 4) {setAttr  cacheBlend"+$j+".fire1Cache"+$j+" 0; setAttr cacheBlend"+$j+".idle1Cache"+$j+" 0; setAttr  cacheBlend"+$j+".run1Cache"+$j+" 0;setAttr cacheBlend"+$j+".die1Cache"+$j+" 1;setAttr  emitter"+$j+".rate 0;} ";
//…и исполняем его при помощи команды eval.
eval ("expression -s \""+ $expr+"\"");

/* Функция eval ($string)
Выполняет строку $string, как если бы ввели ее в Command Line.
Позволяет выполнять  команды, которые могут быть определены только во время исполнения программы.
*/
//Привязываем юнит к плоскости, чтобы он действительно бегал по поверхности, а не парил над ней

geometryConstraint -weight 1 $terr $name;

//Ориентируем юнит относительно нормалей плоскости, чтобы он не передвигался "столбом"

normalConstraint -aimVector 0 1 0 $terr $name ;

//Нам нужна ориентация только по осям X и Z, поэтому отсоединяем Y
disconnectAttr ($name+"_normalConstraint1.constraintRotateY")  ($name+".rotateY");
//Применяем начальное вращение
select $name;
rotate  0 $rot 0;
/* Команда rotate ([-флаги] $x $y $z $object)
Поворачивает объект $object на угол $x по оси x, на $y по оси y и на $z по оси z.
Интересующие нас флаги:
-a(absolute) – Абсолютное вращение. Значения $x, $y и $z заменяют текущие значения поворота объекта,
-r(relative) – Относительное вращение. Значение $x, $y и $z прибавляются к текущим значениям поворота.
Если опустить флаги, вращение будет абсолютным.
*/
/* В нашем примере я не присваивал юнитам материалов, но вам конечно понадобится эта возможность. Присвоить шэйдинг-группу объекту можно с помощью такого кода:
select $name;
sets -e -forceElement blinn1SG;
Где blinn1SG – имя шейдинг-группы вашего материала.
Возможно вам понадобится задавать для каждого типа юнитов отдельный материал. В этом случае нужно отредактировать объявление процедур addUnits и placeUnits. Также не забудьте переписать строчки вызова процедуры addUnits из placeUnits.
*/
//Возвращаем имя только что созданного юнита для последующих издевательств над ним.
return $name;
} // End of "addUnit" PROC
////////////////////////////////////////////////
//Процедура, которая по заданным параметрам создает и размещает юниты на указанном плоском предмете.
global proc placeUnits (string $path, string $style, vector $a1, vector $a2, int $team, float $speed, int $N, float $rov, float $range, float $rot, float $static, string $emitter_attr, string $vis_bullets) {
/*
Аргументы:
$path - Путь до каталога, в котором расположены файлы проекта:
         ref.mb - референс модели,
         fire1.mc(.xml) - геометрический кэш анимации атаки,
         idle1.mc(.xml) - кэш бездействия,
         run1.mc(.xml) - кэш бега,
         die1.mc(.xml) - кэш смерти,
$terr - Имя объекта в сцене, представляющего земную поверхность.
$style - Тип заполнения юнитами поверхности. Пока только один вариант: rectRandom – Случайное распределение внутри прямоугольника размерами 2*$rx на 2*$rz с центром в точке ($ox; $oz) в мировой системе координат.
$team – Номер команды юнита. Юниты одной команды не будут атаковать друг друга.
$speed – Скорость перемещения, разворота юнита. Применяется как коэффициент.
$N – Кол-во создаваемых юнитов.
$rov – Радиус в котором юнит заметит противника и бросится атаковать.
$rot – Изначальная ориентация юнита, в градусах.
$emitterattr – Полная команда для создания эмиттера частиц, который отвечает за характер наносимых юнитом повреждений.
$vis_bullets – Имя ноды, содержащей частицы, применяемые для визуализации пуль. Оставьте пустую строку, если не хотите привязывать дополнительные частицы, например для рукопашных юнитов.
*/
global int $last;
int $j; // Переменная-счетчик для цикла.
string $name; // Имя только что добавленного юнита.
//Для разных "стилей" размещения выполняются разные команды.
if ($style == "rectRandom") { // Start of "rectRandom" IF
         // $a1.x - Координата x центра прямоугольника.
         // $a1.z - Координата z центра прямоугольника.
         // $a2.x - Величина случайного распределения по оси x.
         // $a2.z - Величина случайного распределения по оси z.
         //Запускаем цикл с количеством итераций, равным $n.
         for ($j = 1+$last; ($j <= ($N+$last));$j++){ // Start of "Щас понаставим юнитов" FOR
                   // Используем процедуру addunit, чтобы импортировать модель, задать параметры и атрибуты юнита.
                   $name = addUnit ($j, $path, $team, $speed, $N, $rov, $range, $rot, $static, $emitter_attr, $vis_bullets);
                   // Перемещаем юнит на случайное место внутри прямоугольника.
                   select $name;
                   move -x ($a1.x+rand(-$a2.x,$a2.x)) -z ($a1.z+rand(-$a2.z,$a2.z));
         }; //end of "Щас понаставим юнитов" FOR
}; // End of "rectRandom" IF
if ($style == "lineStraight") { // Start of "lineStraight" IF
         float $lx=$a1.x, $lz=$a1.z; // Задаем начальную позицию.
         // $a1.x - Координата x начала построения линии.
         // $a1.z - Координата z начала построения линии.
         // $a2.x - Расстояние между двумя соседними юнитами. Компонента x.
         // $a2.z - Расстояние между двумя соседними юнитами. Компонента z.
         for ($j = 1+$last; ($j <= ($N+$last));$j++){ // Start of "Щас понаставим юнитов" FOR
                   $name = addUnit ($j, $path, $team, $speed, $N, $rov, $range, $rot, $static, $emitter_attr, $vis_bullets);
                   select $name;
                   move -x $lx -z $lz;
                   // Для каждого следующего юнита позиция будет изменяться с шагом, определенным аргументом $a2, вместе с вспомогательными переменными $lx и $lz.
                   $lx += $a2.x;
                   $lz += $a2.z;
         }; // End of "Щас понаставим юнитов" FOR
}; // End of "lineStraight" IF
//Конечно, вы можете сами написать свой тип размещения юнитов, к примеру – по окружности (одна армия окружила другую), по дуге, даже согласно grayscale-карте. Вариантов множество.
//Неплохая идея – использование Paint Scripts Tool для размещения юнитов.
$last += $N; // Увеличиваем глобальный счетчик кол-ва юнитов.
} // End of "placeUnits" PROC
////////////////////////////////////////////////
//Процедура возвращает расстояние между двумя точками с заданными координатами.
global proc float dist (float $x1, float $y1, float $x2, float $y2){
return sqrt ( pow (($x1-$x2),2)  + pow (($y1-$y2),2)); // По Теореме Пифагора =)
/* Функция pow ($value, $n)
Возвращает число $value, возведенное в степень $n.
*/
} // End of "dist" PROC
////////////////////////////////////////////////
//Процедура ищет ближайший юнит и возвращает его номер.
global proc int nearestUnit (int $this_n, string $type){
/*
Аргументы:
$this_n - Относительно этого юнита производится поиск.
$type - Тип поиска:
         "enemy" - только враги.
         "friend" - только друзья.
*/
global int $last;
int $j, $nearest_n;
float $dist, $mindist=999999;
for ($j=1; $j<=$last; $j++){ // Start of "Перебор юнитов" FOR
         if ($j == $this_n) continue; // Исключаем исходный юнит.
         if ( getAttr(convName($j)+".state") == 4)  continue; // Исключаем "мертвые" юниты.
         // В зависимости от типа поиска исключаются или юниты той же команды, или юниты другой команды.
         if ($type == "enemy") {if (getAttr(convName($j)+".team") == getAttr(convName($this_n)+".team"))  ontinue;}
         if ($type == "friend") {if (getAttr(convName($j)+".team") != getAttr(convName($this_n)+".team")) continue;}
         // Находим расстояния до очередного юнита.
         $dist = dist (getAttr(convName($j)+".tx"), getAttr(convName($j)+".tz"), getAttr(convName($this_n)+".tx"), getAttr(convName($this_n)+".tz"));
         if ($dist < $mindist) {$mindist=$dist; $nearest_n = $j;} // С каждым проходом цикла новая дистанция сравнивается с хранящейся в переменной $mindist. Если новая дистанция меньше, то она заносится в переменную. Также запоминается номер юнита, до которого наименьшее расстояние на данный момент.
} // End of "Перебор юнитов" FOR
//В итоге в переменной $nearest_n остается номер ближайшего юнита, а в $mindist - наименьшее расстояние.
if ($mindist > (getAttr(convName($this_n)+".rov"))) return 0; else return $nearest_n; //если наименьшее расстояние больше rov юнита (радиуса активности), то процедура возвращает "0" (Наш юнит пока не замечает врага).
} // End of "nearestUnit" PROC
////////////////////////////////////////////////
//Процедура возвращает угол, равный аргументу, только в пределах [-180;180].
//Предполагается, что аргумент будет в пределах [-360;360].
global proc float normAng (float $ang) {
if ($ang > 180) $ang = $ang-360;
if ($ang < -180) $ang = $ang+360;
return $ang;
} // End of "normAng" PROC
////////////////////////////////////////////////
//Процедура вычисляет добавочный угол, на который необходимо повернуться юниту, чтобы нацелиться на врага.
global proc float findAngle (int $this_n, int $target_n){
/*
Аргументы:
$this_n - Расчет производится относительно этого юнита.
$target_n - Ближайший враг, найденный с помощью процедуры "nearestUnit".
Процедура учитывает столкновение юнита с членами его же команды. Столкновения с врагами не учитываются.
*/
float $addit_ang, $dist;
//Найдем расстояние по осям x и z для наших юнитов (Вычислим координаты вектора от $this_n до $target_n).
float $z = (getAttr( convName($target_n) + ".tz")-getAttr( convName($this_n) + ".tz") ) ;
float $x = (getAttr( convName($target_n) + ".tx")-getAttr( convName($this_n) + ".tx") ) ;
//Найдем угол между юнитами.
$addit_ang = atan2d ($x, $z);
/* Функция atan2d ($x, $z)
Рассчитывает арктангенс значения $x/$z. Возвращает угол в градусах в пределах [-180, 180].
*/
//Учтем столкновения с юнитам своей команды.
//Для начала ищем ближайший союзный юнит - будут рассчитываться столкновения только с ним.
int $nearest_friend_n = nearestUnit ($this_n, "friend");
//Если в радиусе rov (радиус обзора) есть союзный юнит…
if ($nearest_friend_n != 0) { // Start of "Есть союзник поблизости?" IF
         // Ищем расстояния до ближайшего союзника.
         $dist = dist (getAttr(convName($this_n) + ".tx"), getAttr(convName($this_n) + ".tz"),
         getAttr(convName($nearest_friend_n) + ".tx"), getAttr(convName($nearest_friend_n) + ".tz"));
         if ($dist(getAttr(convName($this_n)+".rov")/3)) { // Если расстояние до него меньше, чем значение rov/3, то…
                   // Введем переменную $col_ang, равную разности: угол, на который нужно повернуться юниту, чтобы нацелиться на врага($addit_ang) `минус` угол, на который нужно повернуться, чтобы нацелиться на ближайшего союзника(atan2d…).
                  $z = (getAttr( convName($nearest_friend_n) + ".tz")-getAttr( convName($this_n) + ".tz") );
                   $x = (getAttr( convName($nearest_friend_n) + ".tx")-getAttr( convName($this_n) + ".tx") );
                   float $col_ang = $addit_ang - atan2d ($x,$z);
                   if (abs($col_ang)<45) { // Если союзник находится в пределах [-45;45] градусов, то…
                   if ($col_ang <= 0) $addit_ang += -(1000/$dist); // В зависимости от знака $col_ang или уменьшаем,..
                   if ($col_ang > 0) $addit_ang += (1000/$dist); // …или увеличиваем угол $addit_ang.
                   // Здесь для определения знака $col_ang можно было использовать функцию sign, которая возвращает знак числа. Однако, если угол равен 0, то функция возвращает 0, поэтому все равно пришлось бы вводить доп.условия для этого случая.
}
} // End of "Есть союзник поблизости?" IF
//Находим относительный угол поворота и …
$addit_ang = $addit_ang - getAttr(convName($this_n) + ".ry");
$addit_ang = normAng ($addit_ang);
// …и возвращаем его.
return $addit_ang;
} // End of "findAngle" PROC

дизайнер компьютерной графики
Иванычев Дмитрий Сергеевич

Бесплатный хостинг uCoz