Анимация
Бывает двух видов, первая это анимация FIREFORGET - последовательность движений выполняемых один раз, и вторая LOOPING – циклическое повторение движений. Кроме того, анимация имеет свою скорость (float fSpeed=1.0) и может быть записана как акция или как действие:
// Cause the action subject to play an animation
// - nAnimation: ANIMATION_*
// - fSpeed: Speed of the animation
// - fDurationSeconds: Duration of the animation (this is not used for Fire and
// Forget animations)
void ActionPlayAnimation(int nAnimation, float fSpeed=1.0, float fDurationSeconds=0.0)
void PlayAnimation(int nAnimation, float fSpeed=1.0, float fSeconds=0.0)
| Вы, наверное, заметили, что по определению функции время анимации равно нулю. Это не означает, что анимация не будет проиграна, т.к. есть еще время заданное движком (недоступно для редактирования), и если время LENGTH меньше установленного, то время анимации возьмется по определению. Это время называется: DESIRED_LENGTH (желательное время). |
| Примерная временная схема анимации типа FIREFORGET при скорости = 1.0f и общем времени LENGTH <= DESIRED_LENGTH. Если LENGTH > DESIRED_LENGTH, то для анимаций:
ANIMATION_FIREFORGET_DRINK
ANIMATION_FIREFORGET_READ
- остаток времени займет пауза. Сделать паузу между другими анимациями FIREFORGET можно с помощью функции:
// Do nothing for fSeconds seconds.
void ActionWait(float fSeconds)
|
| Примерная временная схема анимации типа LOOPING. Как видно из схемы основной ролик растягивается почти на все время LENGTH, за исключением начального и конечного ролика. |
ВНИМАНИЕ для констант FIREFORGET без вспомогательного предмета (свиток, бутылка) регулируется только скорость PLAYBACK SPEED, при этом, какое бы вы не выставили время LENGTH, время проигрывания определено движком и равно DESIRED_LENGTH. Причем, для большинства FIREFORGET эффект изменения скорости PLAYBACK SPEED заметен…
Например:
ActionPlayAnimation(ANIMATION_FIREFORGET_GREETING, 1.0f, 10.0f);
ActionPlayAnimation(ANIMATION_FIREFORGET_PAUSE_SCRATCH_HEAD, 1.0f, 10.0f);
ничем не отличаются при проигрывании от:
ActionPlayAnimation(ANIMATION_FIREFORGET_GREETING);
ActionPlayAnimation(ANIMATION_FIREFORGET_PAUSE_SCRATCH_HEAD);
С изменением скорости анимации Playback Speed, меняется и желательное время DESIRED_LENGTH. Если мы увеличиваем скорость (желательный диапазон 1.0 – 3.0), то время DESIRED_LENGTH уменьшается, но при этом стоит не забывать, что время нормальной анимации длится примерно 2 секунды, а при увеличении скорости часть кадров может срезаться и мы ничего не увидим...
Скорость можно и уменьшить, при значениях меньше единицы, время DESIRED_LENGTH увеличивается, и анимация замедлятся. Рискну предположить, что все основное изменение происходит в интервале основного ролика, поэтому не вся анимация FIREFORGET подвержена изменению скорости…
Например анимация ANIMATION_FIREFORGET_DRINK при скорости 0.2, растягивает DESIRED_LENGTH примерно до 8 - 10 секунд. NPC c нормальной скоростью глотнет из бутылки и опустит руку с бутылкой вниз, причем бутылка остается в руке NPC еще некоторое время, и исчезает при завершающем ролике. Анимация ANIMATION_FIREFORGET_GREETING вообще существенно не подвержена изменению скорости анимации, в отличие от ANIMATION_FIREFORGET_BOW или ANIMATION_FIREFORGET_PAUSE_SCRATCH_HEAD, но при этом время DESIRED_LENGTH всегда изменяется. Так что при изменение скорости, стоит всегда проверять, что у вас реально получилось…
Для наглядности запишем скрипт для НПС, который ставим в слот OnHeartbeat:
//:://////////////////////////////////////////////////
//:: Слот: OnHeartbeat
//:: File name: anim_1
//:://////////////////////////////////////////////////
void main()
{
object oSelf = OBJECT_SELF; // Наш НПС
if(GetLocalInt(oSelf, "ZIKL")==1) return; // ВЫХОД ИЗ СКРИПТА
ActionSpeakString("НОРМАЛЬНАЯ СКОРОСТЬ");
ActionPlayAnimation(ANIMATION_FIREFORGET_DRINK);
ActionPlayAnimation(ANIMATION_FIREFORGET_GREETING);
ActionPlayAnimation(ANIMATION_FIREFORGET_PAUSE_SCRATCH_HEAD);
ActionSpeakString("ЗАМЕДЛЕННАЯ СКОРОСТЬ");
ActionPlayAnimation(ANIMATION_FIREFORGET_DRINK, 0.2);
ActionPlayAnimation(ANIMATION_FIREFORGET_GREETING, 0.3);
ActionPlayAnimation(ANIMATION_FIREFORGET_PAUSE_SCRATCH_HEAD, 0.3);
ActionSpeakString("УВЕЛИЧЕННАЯ СКОРОСТЬ");
ActionPlayAnimation(ANIMATION_FIREFORGET_DRINK, 2.0);
ActionWait(1.0);
ActionPlayAnimation(ANIMATION_FIREFORGET_GREETING, 2.0);
ActionWait(1.0);
ActionPlayAnimation(ANIMATION_FIREFORGET_PAUSE_SCRATCH_HEAD, 2.0);
// УВЕЛИЧЕНИЕ ЦИКЛА
SetLocalInt(oSelf, "ZIKL", 1); // ПРИСВОИТЬ ЛОКАЛКУ
DelayCommand(35.0, SetLocalInt(oSelf, "ZIKL", 0)); //СНЯТЬ ЛОКАЛКУ
}
|
Ниже приведена таблица анимационных констант для версии 1.32 с кратким описанием анимации НПС.
АНИМАЦИОННАЯ КОНСТАНТА | Описание действия |
ANIMATION_LOOPING_PAUSE | NPC стоит, ничего не делает, полная пауза |
ANIMATION_LOOPING_PAUSE2 | NPC стоит, ничего не делает, полная пауза |
ANIMATION_LOOPING_LISTEN | NPC слушает, временами кивает |
ANIMATION_LOOPING_MEDITATE | NPC встает на колени и складывает руки перед собой |
ANIMATION_LOOPING_WORSHIP | NPC встает на колени и кланяется |
ANIMATION_LOOPING_LOOK_FAR | NPC прикладывает руку ко лбу, имитируя взгляд в даль |
ANIMATION_LOOPING_SIT_CHAIR | NPC имитирует сидение на стуле |
ANIMATION_LOOPING_SIT_CROSS | NPC садится на пол скрестив ноги |
ANIMATION_LOOPING_TALK_NORMAL | NPC имитирует нормальный разговор |
ANIMATION_LOOPING_TALK_PLEADING | NPC умоляюще протягивает руки |
ANIMATION_LOOPING_TALK_FORCEFUL | NPC убеждающе машет руками |
ANIMATION_LOOPING_TALK_LAUGHING | NPC смеется |
ANIMATION_LOOPING_GET_LOW | Анимация поднять предмет |
ANIMATION_LOOPING_GET_MID | NPC как бы поворачивает рукой дверную ручку |
ANIMATION_LOOPING_PAUSE_TIRED | NPC потягивает спину, немного заломив за нее руки |
ANIMATION_LOOPING_PAUSE_DRUNK | NPC пьяно покачивается |
ANIMATION_LOOPING_DEAD_FRONT | NPC падает на колени, а потом лицом вниз – умер |
ANIMATION_LOOPING_DEAD_BACK | NPC сразу откидывается на спину – умер |
ANIMATION_LOOPING_CONJURE1 | Похоже на боевую стойку или на защиту |
ANIMATION_LOOPING_CONJURE2 | NPC поднимает руки над головой и покачивает ими |
ANIMATION_LOOPING_SPASM | У NPC как бы спазмы, еще это очень похоже на танец |
ANIMATION_FIREFORGET_HEAD_TURN_LEFT | NPC поворачивает голову влево |
ANIMATION_FIREFORGET_HEAD_TURN_RIGHT | NPC поворачивает голову вправо |
ANIMATION_FIREFORGET_PAUSE_SCRATCH_HEAD | NPC чешет голову |
ANIMATION_FIREFORGET_PAUSE_BORED | NPC слегка покачивается, как бы от усталости |
ANIMATION_FIREFORGET_SALUTE | NPC отдает честь - приветствует |
ANIMATION_FIREFORGET_BOW | NPC кланяется в пояс |
ANIMATION_FIREFORGET_STEAL | NPC имитирует воровские движения |
ANIMATION_FIREFORGET_GREETING | NPC приветственно машет рукой |
ANIMATION_FIREFORGET_TAUNT | NPC насмехается |
ANIMATION_FIREFORGET_VICTORY1 | NPC радуется, поднимая вверх одну руку |
ANIMATION_FIREFORGET_VICTORY2 | NPC радуется, расставляя руки в стороны и вверх |
ANIMATION_FIREFORGET_VICTORY3 | NPC радуется, несколько раз поднимает руку вверх |
ANIMATION_FIREFORGET_READ | NPC достает свиток и читает его |
ANIMATION_FIREFORGET_DRINK | NPC достает бутылку и пьет |
ANIMATION_FIREFORGET_DODGE_SIDE | Сначала NPC нагинается, а потом отходит в сторону |
ANIMATION_FIREFORGET_DODGE_DUCK | Сначала NPC нагинается, а потом отходит в сторону |
ANIMATION_PLACEABLE_ACTIVATE | Анимация активации (рычаги, кнопки, факелы итд.) |
ANIMATION_PLACEABLE_DEACTIVATE | Анимация деактивации (рычаги, кнопки, факелы итд.) |
ANIMATION_PLACEABLE_OPEN | Анимация открывания (сундук, ящик, саркофаг итд.) |
ANIMATION_PLACEABLE_CLOSE | Анимация закрывания (сундук, ящик, саркофаг итд.) |
Если анимация применяется к владельцу скрипта (OBJECT_SELF), то можно просто записывать эти функции, а вот если нужно задать анимацию другому объекту, то нужно использовать оператор AssignCommand. В обоих случаях можно использовать оператор DelayCommand, для точного определения времени начала анимации. Пример:
//:://////////////////////////////////////////////////
//:: Слот: OnHeartbeat
//:: File name: anim_2
//:://////////////////////////////////////////////////
void main()
{
object oSelf = OBJECT_SELF; // Наш НПС
if(GetLocalInt(oSelf, "ZIKL")==1) return; // ВЫХОД ИЗ СКРИПТА
object oNPC = GetNearestObjectByTag("NPC_ANIMATION_01");
// 1 NPC АНИМАЦИЯ 1 ПРИВЕТСТВИЕ
PlayAnimation(ANIMATION_FIREFORGET_SALUTE);
// ФРАЗА 1 NPC
SpeakString("Привет!");
// 2 NPC АНИМАЦИЯ 1 ПРИВЕТСТВИЕ
DelayCommand(3.0, AssignCommand(oNPC, ActionPlayAnimation(ANIMATION_FIREFORGET_BOW)));
// ФРАЗА 2 NPC
DelayCommand(6.0, AssignCommand(oNPC, SpeakString("Наше вам с кисточкой!")));
// 1 NPC АНИМАЦИЯ 2
DelayCommand(10.0, PlayAnimation(ANIMATION_FIREFORGET_PAUSE_SCRATCH_HEAD));
// ФРАЗА 1 NPC
DelayCommand(12.0, SpeakString("Ну, и что это за приветствие?"));
// УВЕЛИЧЕНИЕ ЦИКЛА
SetLocalInt(oSelf, "ZIKL", 1); // ПРИСВОИТЬ ЛОКАЛКУ
DelayCommand(15.0, SetLocalInt(oSelf, "ZIKL", 0)); //СНЯТЬ ЛОКАЛКУ
}
|
НАЛОЖЕНИЕ АНИМАЦИЙ ПРИ ПОМОЩИ ОЧЕРЕДИ ДЕЙСТВИЙ
(только для ActionPlayAnimation)
Накладывать можно только FIREFORGET на LOOPING. Рассмотрим простой пример:
//:://////////////////////////////////////////////////
//:: Слот: OnHeartbeat
//:: File name: anim_3
//:://////////////////////////////////////////////////
void main()
{
object oSelf = OBJECT_SELF; // Наш НПС
if(GetLocalInt(oSelf, "ZIKL")==1) return; // ВЫХОД ИЗ СКРИПТА
ActionPlayAnimation(ANIMATION_LOOPING_MEDITATE, 1.0, 10.0);
ActionPlayAnimation(ANIMATION_FIREFORGET_DRINK, 1.0, 7.0);
ActionPlayAnimation(ANIMATION_FIREFORGET_GREETING, 1.0, 6.0);
ActionPlayAnimation(ANIMATION_FIREFORGET_READ, 1.0, 8.0);
// УВЕЛИЧЕНИЕ ЦИКЛА
SetLocalInt(oSelf, "ZIKL", 1); // ПРИСВОИТЬ ЛОКАЛКУ
DelayCommand(29.0, SetLocalInt(oSelf, "ZIKL", 0)); //СНЯТЬ ЛОКАЛКУ
}
|
Реально происходит следующее:
- Проигрывается начальный ролик ANIMATION_LOOPING_MEDITATE длиной T_BEG
- Проигрывается основной ролик ANIMATION_LOOPING_MEDITATE длиной LENGTH = 10.0 сек., но завершающий ролик не проигрывается, т.е. NPC продолжает "медитировать".
- Проигрывается ANIMATION_FIREFORGET_DRINK, причем реально _DRINK проигрывается DESIRED_LENGTH сек, а оставшееся время (7.0 - DESIRED_LENGTH) сек. NPC продолжает "медитировать".
- Проигрывается ANIMATION_FIREFORGET_GREETING, причем реально _GREETING проигрывается DESIRED_LENGTH сек, а не 6.0. (см. FIREFORGET без предмета)
- Проигрывается ANIMATION_FIREFORGET_READ, по тем же правилам, что и DRINK: т.е. NPC снова "медитирует" после чтения (8.0 - DESIRED_LENGTH) сек.
- Проигрывается завершающий ролик _LOOPING длиной T_END. ТОЛЬКО СЕЙЧАС NPC встанет.
НЕБОЛЬШОЕ ДОПОЛНЕНИЕ:
Если в каком-либо из ANIMATION_FIREFORGET_* будут задействованы ноги NPC, то он обязательно встанет для выполнения этого действия, а по завершению снова сядет.
По материалам статьи Lex & LexxL, оригинал см. здесь...
Хочу напомнить, как идет запись команд для НПС. Каждый НПС имеет свой стек, т.е. лист, куда ему можно записать команды посредством скрипта. Перед началом записи команд, нужно сперва очистить стек, ведь базовые скрипты могут загрузить стек НПС на выполнение каких либо команд, например, ходьбы или реакции на атаку. Для этого служит функция очистки акций записанных на НПС:
ClearAllActions(int nClearCombatState=FALSE)
инт в скобочках равен по умолчанию FALSE – т.е. нулю. Если поставить TRUE, то это будет полная чистка акций, и даже той команды, которая непосредственно выполняется. Например, при атаке, или для героя, когда нужно остановить его движение. Для последовательного исполнения команд или фунций, нужно чтобы все функции были акциями, т.е. начинающимися на Action. Понятно, что не все нужные функции начинаются с Action, тогда можно просто функцию (обязательно с оператором void) перевести в акцию посредством команды ActionDoCommand. Например:
ActionDoCommand(PlaySound("Sound"));
Добавлю, что функция может быть сложной, состоящей из нескольких, но прописав ее таким образом, вы все равно поставите ее в строй. Можно т.ж. записать ряд функций через определение точного времени их выполнения. Для этого служит команда:
DelayCommand(float fSeconds, action aActionToDelay)
где флот время в секундах и акция, которая будет выполняться. Причем хочу отметить, что если функции прописаны через время, то их нельзя почистить, и они будут выполнятся безусловно, главное чтобы не погиб НПС на котором мы запишем скрипт. Понятно, что просчитать все время невозможно, да если при этом НПС будет выполнять еще и другие команды, то получится полная неразбериха… Поэтому для НПС строй акций самое приемлемое решение. Чтобы не нарушить этот строй служат команды:
SetCommandable(FALSE, oPC); // заблокировать очередь
SetCommandable(TRUE, oPC); // разблокировать очередь
Например, если игрок кликнет по НПС для старта диалога, то скрипт в слоте OnConversation проведет чистку акций, и весь строй акций очистится.
Теперь попробуем записать простенький строй из трех акций:
//:://////////////////////////////////////////////////
//:: Слот: OnHeartbeat
//:: File name: stroy_1
//:://////////////////////////////////////////////////
void main()
{
object oSelf = OBJECT_SELF; // Наш НПС
ClearAllActions(); // Почистим стек
ActionPlayAnimation(ANIMATION_LOOPING_LOOK_FAR, 1.0, 5.0); // 1 АКЦИЯ
ActionSpeakString("Да-а, видно дождик будет…"); // 2 АКЦИЯ
ActionPlayAnimation(ANIMATION_FIREFORGET_PAUSE_SCRATCH_HEAD); // 3 АКЦИЯ
ActionDoCommand(SetCommandable(TRUE, oSelf)); // разблокировать очередь
DelayCommand(0.1, SetCommandable(FALSE, oSelf)); // заблокировать очередь
}
|
Как работает скрипт? Для начала мы определим, что наш НПС является носителем этого скрипта. Потом почистим ему стек команд. Дальше мы записываем наш строй акций, их три. Чтобы не сбить наши акции, я добавил еще две строки. Предпоследняя строка является тоже акцией снятия блокировки НПС от влияния внешний воздействий. Последняя строка полная блокировка НПС прописана через время, т.е. она сработает, когда мы уже запишем все наши команды в стек.
В стек команд НПС можно также записать и действия для других НПС или героя. Для этого служит функция:
AssignCommand(object oActionSubject, action aActionToAssign)
где мы определяем объект и акцию, которую этот объект должен выполнить. Если скрипт записан на НПС, то можно не определять кому выполнять акции, по умолчанию это будет владелец скрипта. Если это другой персонаж, то нужно обязательно подключать эту функцию определения НПС. Например подключим еще одного НПС к диалогу в верхний скрипт. Хочу еще отметить, что функция DelayCommand тоже может стать в очередь, как акция. Пример подключения в скрипт другого НПС:
//:://////////////////////////////////////////////////
//:: Слот: OnHeartbeat
//:: File name: stroy_2
//:://////////////////////////////////////////////////
void main()
{
object oSelf = OBJECT_SELF; // Наш НПС
object oNPC = GetNearestObject(OBJECT_TYPE_CREATURE); // Ближайший НПС
ClearAllActions(); // Почистим стек
ActionPlayAnimation(ANIMATION_LOOPING_LOOK_FAR, 1.0, 5.0); // 1 АКЦИЯ
ActionSpeakString("Да-а, видно дождик будет…"); // 2 АКЦИЯ
ActionWait(5.0); // 3 АКЦИЯ - ПАУЗА
ActionDoCommand(AssignCommand(oNPC, SpeakString("Ты прав!")));
ActionPlayAnimation(ANIMATION_FIREFORGET_PAUSE_SCRATCH_HEAD); // 4 АКЦИЯ
ActionDoCommand(AssignCommand(oNPC, ClearAllActions()));
// oNPC подходит к нашему НПС
ActionDoCommand(AssignCommand(oNPC, DelayCommand(0.5, ActionMoveToObject(oSelf))));
ActionDoCommand(SetCommandable(TRUE, oSelf)); // разблокировать очередь
DelayCommand(0.1, SetCommandable(FALSE, oSelf)); // заблокировать очередь
}
|
Можете проверить, что у нас получилось. Я просто добавил еще паузу НПС, и текст для нового НПС и его последующий подход. Достаточно повесить этот скрипт, допустим на слот OnHeartBeat НПС, и поставить рядом еще одного НПС. Если будет рядом герой, то тогда он и подойдет к НПС.
Чтобы как-то оживить персонажей игры, нужно им задать программу, т.е. скрипт с персональной анимацией. Этот скрипт мы ставим в слот OnHeartbeat, который опрашивается движком игры каждые 6 секунд, если в НПС и герой находятся в одной области. Если НПС находится не в одной области с героем, то движок опрашивает таких НПС по всему модулю отдельным циклом, и время опроса зависит от количества этих НПС и оно всегда больше 6 секунд…
В этом цикле нужно учесть несколько блокировок, т.к. это активный скрипт и он может повлиять на игровой процесс. Вот список самых необходимых:
- Блокировка области нахождения НПС.
- Блокировка при атаке НПС другим НПС или героем.
- Блокировка при диалоге НПС с героем.
- Блокировка увеличения времени цикла.
- Блокировка расстояния реакции НПС до героя или другого объекта.
- Блокировка для анимационных сценок.
Блокировка области нахождения НПС
Эта блокировка нужна для сингла, т.к. при большом количестве НПС в модуле, и большими скриптами, может вызвать торможение игры, да и не для кого этим НПС проигрывать анимацию в пустых областях…
if(GetArea(GetFirstPC()) != GetArea(OBJECT_SELF))
Блокировка при атаке НПС другим НПС или героем
Нужна для игровой реальности, если конечно в скрипте используется чистка акций НПС. Вообще атака у НПС запускается со скриптов слотов OnPerception, OnPhysicalAttacked, OnSpellCastAt, OnDamaged и если мы чистим акции с скрипте OnHeartbeat, то мы можем прервать эту атаку, и для ее активации потребуется еще раз запустить эти скрипты, иначе наш НПС займется своими обычными делами во время боя…
if (GetIsInCombat(OBJECT_SELF))
Блокировка при диалоге НПС с героем
Необходима, чтобы при старте диалога НПС не занялся своими делами…
if (IsInConversation(OBJECT_SELF))
Блокировка увеличения времени цикла
Не всегда у нас анимация может вместить с 6 секунд цикла, поэтому нужно как-то время увеличить и поставить на это событие блокировку. Обычно нужно присвоить локалку объекту, а затем посредством оператора DelayCommand обнулить. Если у нас в скрипте идет запись через строй акций, то можно также сделать присвоение локалки через акцию.
if(GetLocalInt(OBJECT_SELF, "ZIKL")==1) // ПРОВЕРИТЬ
SetLocalInt(OBJECT_SELF, "ZIKL", 1); // ПРИСВОИТЬ ЛОКАЛКУ
DelayCommand(35.0, SetLocalInt(OBJECT_SELF, "ZIKL", 0)); // СНЯТЬ ЛОКАЛКУ
ActionDoCommand(SetLocalInt(OBJECT_SELF, "ZIKL", 0)); // АКЦИЯ СНЯТЬ ЛОКАЛКУ
Блокировка расстояния реакции НПС до героя или другого объекта
Нужна в тех случаях, когда нужно привязать работу скрипта НПС к некоторому объекту, или подходу героя на определенное расстояние к НПС.
object oPC = GetNearestCreature(CREATURE_TYPE_PLAYER_CHAR, PLAYER_CHAR_IS_PC);
if (GetDistanceToObject(oPC) < 10.0 && GetIsObjectValid(oPC))
if (GetDistanceBetween(oPC, OBJECT_SELF) < 10.0 && GetIsObjectValid(oPC))
Важно: Расстояние сравнивается только между двумя объектами одной области!
Примечание: Величина оператора GetDistanceToObject для объекта в другой области равняется: –1.00. Значит можно записать такую проверочку для любого объекта:
if (GetDistanceToObject(oPC) < 10.0 && GetDistanceToObject(oPC) >= 0.0)
Блокировка для анимационных сценок
Если вы хотите чтобы НПС участвовал в анимационной сценке, то нужно блокировать скрипт обычной ХБ анимации, ведь сценка может длиться гораздо дольше, чем цикл анимации…
if(GetLocalInt(OBJECT_SELF, "CUTSCENE")==1) // ПРОВЕРИТЬ
SetLocalInt(OBJECT_SELF, "CUTSCENE", 1); // ПРИСВОИТЬ ЛОКАЛКУ
Попробуем теперь записать рабочий скрипт. Например, для молящегося персонажа, с наложением анимации и с двумя блоками управления. Нам еще понадобится одна точка для фиксации местоположения и определение направления для молитвы, к которой НПС должен подойти. Плюс если рядом с ним будет герой, то пусть НПС произнесет слова молитвы. Сам скрипт:
//:://////////////////////////////////////////////////
//:: МОЛЯЩИЙСЯ NPC
//:: Слот: OnHeartbeat
//:: File name: molitva_1
//:://////////////////////////////////////////////////
// object oPC - Главный Герой
// string sMol - Фраза молитвы НПС
// object oNPC - Владелец скрипта
// float fDist - ДистанциЯ между объектами
void Molitva(object oPC, string sMol, object oNPC=OBJECT_SELF, float fDist=10.0);
void Molitva(object oPC, string sMol, object oNPC=OBJECT_SELF, float fDist=10.0)
{
if (GetDistanceBetween(oPC, oNPC) < fDist)
AssignCommand(oNPC, SpeakString(sMol));
}
///////////////////////////////////////////////////////////
void main()
{
object oSelf = OBJECT_SELF; // НАШ НПС
// Выход из скрипта, если ГГ нет в области
if(GetArea(GetFirstPC()) != GetArea(oSelf))
{ClearAllActions(); SetLocalInt(oSelf, "ZIKL", 0); return;}
// ПРОВЕРИТЬ КАТСЦЕНУ
if(GetLocalInt(oSelf, "CUTSCENE")==1)
{SetLocalInt(oSelf, "ZIKL", 0); return;}
// Если говорит выход из скрипта
if(IsInConversation(oSelf))
{SetLocalInt(oSelf, "ZIKL", 0); return;}
// Если атакован выход из скрипта
if(GetIsInCombat(oSelf))
{SetLocalInt(oSelf, "ZIKL", 0); return;}
if(GetLocalInt(oSelf, "ZIKL")==1) return; // ПРОВЕРИТЬ ЦИКЛ
object oPC = GetFirstPC(); // Главный Герой
object oWP = GetNearestObjectByTag("VEC_" + GetTag(oSelf)); // Точка
float fNap = GetFacing(oWP); // Направление НПС
string sMoL1 = "Господи, спаси и сохрани!";
string sMoL2 = "Господи, пощади нас грешных!";
ClearAllActions(); // Почистим стек
SetLocalInt(oSelf, "ZIKL", 1); // ПРИСВОИТЬ ЛОКАЛКУ ЦИКЛA
// Если есть точка и она далее 0.5 метра, то НПС подойдем к ней
if (GetDistanceBetween(oSelf, oWP) > 0.5 && GetIsObjectValid(oWP))
ActionForceMoveToObject(oWP, FALSE, 0.0, 15.0);
ActionDoCommand(SetFacing(fNap)); //Примем правельное направление
//Два случайных блока анимации
switch (d2())
{
case 1:
ActionPlayAnimation(ANIMATION_LOOPING_MEDITATE, 1.0, 5.0);
ActionDoCommand(Molitva(oPC, sMoL1, oSelf, 5.0)); // НОВАЯ ФУНКЦИЯ КАК АКЦИЯ
ActionPlayAnimation(ANIMATION_FIREFORGET_READ, 1.0, 8.0);
ActionPlayAnimation(ANIMATION_LOOPING_MEDITATE, 1.0, 5.0);
break;
case 2:
ActionDoCommand(Molitva(oPC, sMoL2)); // НОВАЯ ФУНКЦИЯ КАК АКЦИЯ
ActionPlayAnimation(ANIMATION_LOOPING_CONJURE2, 1.0, 3.0);
ActionPlayAnimation(ANIMATION_LOOPING_WORSHIP, 1.0, 10.0);
ActionPlayAnimation(ANIMATION_LOOPING_PAUSE2, 1.0, 3.0);
ActionPlayAnimation(ANIMATION_LOOPING_MEDITATE, 1.0, 5.0);
break;
}
// Основной блок скрипта
ActionSpeakString("Аминь!");
ActionPlayAnimation(ANIMATION_LOOPING_PAUSE, 1.0, 5.0);
ActionDoCommand(SetLocalInt(oSelf, "ZIKL", 0)); // АКЦИЯ СНЯТЬ ЛОКАЛКУ
}
/*
Если в локации есть точка с тегом "VEC_" + GetTag(oSelf), то НПС подойдет к ней
Эта точка будет направлением для молитвы НПС
Если рядом с НПС будет Герой, то НПС произнесет молитву
*/
|
В начале скрипта мы определили нашего НПС как oSelf, затем сразу сделали проверку находится ли в области Главный герой. Если героя нет, то мы чистим стек у НПС и обнуляем локалку увеличения цикла, после чего следует, что оператор return прерывает чтение скрипта. Как видите, скрипт для НПС вне области Героя получился всего в три строки и три команды, что существенно снизит загрузку процессора во время игры.
Дальше мы делаем другие проверки, причем во всех обнуляем переменную цикла. Это сделано для того, что в случае прерывания строя акций, вернуть переменной ZIKL нулевое значение. Это важно, т.к. в скрипте обнуление переменной записано, как акция, и она может прерваться при клике НПС, или другому действию. Если бы использовали для возврата значения переменной оператор DelayCommand, то этого бы делать не пришлось, но зато теперь в блоках анимации мы можем произвольно задавать количество и время разных действий НПС.
Если НПС будет участвовать в анимационной сценке, то нужно в скрипте сценки присвоить НПС локалку "CUTSCENE". Это позволить прервать скрипт OnHeartbeat и задать спокойно в сценке нужные действия для этого НПС. После окончания сцены, нужно просто обнулить эту локалку для возврата НПС к своим обязанностям.
После блокировок, задаем переменные операторы. Точку мы привязали к тэгу НПС, что позволит этот же скрипт поставить на любое количество подобных персонажей. Просто нам останется поменять тэги НПС и теги точек, на которых они должны молиться. Как видите, четыре начальных символа VEC_ точки остаются постоянными, а окончание будет тэг нужного НПС. Например, такой тэг точки: VEC_NPC_Mol_001, где NPC_Mol_001 это тэг самого НПС.
Далее, мы определяем направление, т.е. флот этой точки, чтобы в дальнейшем задать такое же и нашему НПС. Это значение мы задаем при помощи функции GetFacing. Далее задаем две фразы для молитвы, фраз может быть сколько угодно…
Основной блок скрипта начинаем с чистки стека НПС, и сразу же присваиваем переменной цикла ZIKL, значение единицы. Этим мы блокируем 6 секундный опрос движка, т.к. выше в скрипте у нас уже стоит блокировка цикла, которая и прервет чтение скрипта при значении этой переменной равной единицы.
Для выбора блока анимации, используем оператор switch, в условие которого ставим оператор случайного числа – дайс d2(), что позволит нам получить два числовых значения 1 или 2. Эти величины и отражаем в блоках case.
После блоков анимации, идет акция вывода еще одной фразы НПС, анимация паузы диалога, и завершение скрипта обнуление переменной цикла ZIKL. Вот и вся работа скрипта, осталось только разобрать действие новой функции Molitva, которую я использовал в блоках анимации.
В новой функции мы задаем 4 оператора, два последних с определенным значением. В этой функции нет акций, и мы смело можем ее использовать в любом месте первого блока анимации, где идет совмещение анимации молитвы и чтения свитка. В этом блоке я задал все значения, в том числе изменил расстояние до 5 метров. Определение расстояния я задал простой проверкой, т.к. в начале скрипта мы уже определили, что НПС и герой находятся в одной области. Во втором блоке анимации у функции Molitva, используются значения по определению, т.е. расстояние берется 10 метров.
Скриптовые ролики начинаются с команды SetCutsceneMode(object oCreature, int nInCutscene=TRUE), и значением nInCutscene равным по определению TRUE. По окончании сценки нужно обязательно вернуть этот оператор к значению FALSE.
SetCutsceneMode(oPC, TRUE); // Старт ролика
DelayCommand(Time, SetCutsceneMode(oPC, FALSE)); // Конец ролика
Во время ролика, герой полностью лишается управления, но скрипты прописанные для других НПС продолжают работать, и этот фактор нужно учитывать при составлении скрипта ролика. Во время сценки камера движка всегда привязана к герою, а значит нужно правильно задать точку камеры и выбрать нужный режим. Чтобы правильно отобразилась сценка, вне зависимости какие сделает установки игрок, желательно перед сценкой, или если нужно во время сценки, задать нужный режим. Для этого мы используем функцию:
// object oPlayer – объект к которому нужно применить функцию
// int nCameraMode – режим с константой CAMERA_MODE
SetCameraMode(object oPlayer, int nCameraMode)
SetCameraMode(oPC, CAMERA_MODE_TOP_DOWN); // режим сверху вниз
У нас есть три режима привязки камеры:
- CAMERA_MODE_CHASE_CAMERA – следящая камера
- CAMERA_MODE_STIFF_CHASE_CAMERA – управляемая камера
- CAMERA_MODE_TOP_DOWN – сверху вниз
Обычно большинство игроков предпочитают для игры третий режим “Сверху вниз”, а для более крупного плана в ролике можно ставить режим “Управляемая камера”.
Иногда нужно скрыть от глаз игрока перемещение персонажей ролика, для этого служит заставка черного экрана:
FadeToBlack(object oCreature, float fSpeed=FADE_SPEED_MEDIUM)
FadeToBlack(oPC, FADE_SPEED_FASTEST); // Занавес
где мы можем менять скорость завесы, меняя константы.
Для того, чтобы убрать черный экран используем функцию:
FadeToBlack(object oCreature, float fSpeed=FADE_SPEED_MEDIUM)
DelayCommand(Time, FadeFromBlack(oPC, FADE_SPEED_MEDIUM)); // Уберем занавес
где мы тоже можем менять скорость возврата от черного экрана, к обычному режиму.
Для выбора ракурса камеры, т.е. ее положения относительно героя, нужна функция:
// fDirection – 0.0-360.0 [Восток =0.0 (правая), Север=90.0 (верх), Запад=180.0 (левая), Юг=270.0 (низ)]
// fDistance - Удаление от героя = 5-20
// fPitch - Наклон = 1-50 (1 – максимально вверх, 50 максимально вниз)
// nTransitionType: CAMERA_TRANSITION_TYPE_SNAP немедленно переместит камеру на новую позицию
SetCameraFacing(float fDirection, float fDistance = -1.0f, float fPitch = -1.0, int nTransitionType= CAMERA_TRANSITION_TYPE_SNAP)
Важно: Эту функцию нужно обязательно применять с оператором AssignCommand и привязкой к герою.
AssignCommand(oPC, SetCameraFacing(270.0, 7.0, 50.0, CAMERA_TRANSITION_TYPE_VERY_FAST)); // Задать ракурс
В скрипте ролика нужно обязательно учитывать команду героя, т.к. у них работает прыжок на ХБ, и они все время норовят прыгнуть к герою. Поэтому им нужно кинуть эффект неподвижности, если конечно они не являются активными участниками сценки:
effect ePar = EffectCutsceneParalyze(); // Эффект парализации
Иногда нужно переместить камеру в удаленный уголок области, и при этом, чтобы герой был невидим. В этом случае используем эффект полной невидимости:
effect eInvis = EffectVisualEffect(VFX_DUR_CUTSCENE_INVISIBILITY); // ПОЛНАЯ невидимость
Обычно все эффекты парализации, или другие функции, накладываем с заданием определенного времени, а по мере написания скрипта, время приходиться не раз корректировать. Чтобы не делать лишней работы, нужно сразу задать определенное время: float Time = 100.0; и при необходимости корректировать только его.
Мы уже разобрали все необходимые функции для скрипта ролика, теперь попробуем записать короткую сценку. Сюжет такой: Герой в диалоге предлагает отойти НПСу в укромный уголок, где они и продолжат дальнейшую беседу. Нам потребуется задать в области три точки для прыжка: Героя, НПС и спутников героя. Сам скрипт ролика:
//::////////////////////////////////////////
//:: СЦЕНКА УЕДИНЕННОГО ДИАЛОГА
//:: FileName: cutscene_1
//::////////////////////////////////////////
void main()
{
object oSelf = OBJECT_SELF;
object oPC = GetPCSpeaker();
object oNPC = GetFirstObjectInArea(oPC);
object oWP1 = GetNearestObjectByTag("WP_CAMERA_SELF");
object oWP2 = GetNearestObjectByTag("WP_CAMERA_PC");
object oWP3 = GetNearestObjectByTag("WP_CAMERA_NPC ");
effect ePar = EffectCutsceneParalyze(); // Эффект парализации
float Time = 22.0;
// *** УСТАНОВКИ ***
while (GetIsObjectValid(oNPC)) // ДЛЯ СПУТНИКОВ ГЕРОЯ
{
if (oPC==GetMaster(oNPC)) // ЕСЛИ ГЕРОЙ ХОЗЯИН oNPC
{
AssignCommand(oNPC, ClearAllActions()); // Чистим стек
AssignCommand(oNPC, ActionJumpToObject(oWP3)); // Прыжок к точке
// Эффект парализации
AssignCommand(oNPC, ActionDoCommand(ApplyEffectToObject
(DURATION_TYPE_TEMPORARY, ePar, oNPC, Time))); // ЭФФЕКТ - КАК АКЦИЯ
DelayCommand(2.0, ApplyEffectToObject(DURATION_TYPE_TEMPORARY, ePar, oNPC, Time-2.0));
}
oNPC = GetNextObjectInArea(oPC);
}
SetLocalInt(oSelf, "CUTSCENE", 1); // Блокируем ХБ скрипт
SetCameraMode(oPC, CAMERA_MODE_TOP_DOWN); // установим режим камеры
FadeToBlack(oPC, FADE_SPEED_FASTEST); // Занавес
// Поставим персонажи по точкам сценки
AssignCommand(oPC, ClearAllActions());
AssignCommand(oPC, ActionJumpToObject(oWP2));
DelayCommand(0.3, AssignCommand(oSelf, ClearAllActions()));
DelayCommand(0.31, AssignCommand(oSelf, ActionJumpToObject(oWP1)));
// *** СТАРТ РОЛИКА ***
DelayCommand(0.5, SetCutsceneMode(oPC, TRUE)); // Режим ролика
DelayCommand(1.0, AssignCommand(oPC, SetCameraFacing(270.0, 7.0, 50.0,
CAMERA_TRANSITION_TYPE_VERY_FAST))); // Ракурс камеры быстро
DelayCommand(2.0, FadeFromBlack(oPC, FADE_SPEED_MEDIUM)); // Уберем
DelayCommand(2.5, AssignCommand(oPC, SetCameraFacing(270.0, 5.0, 50.0,
CAMERA_TRANSITION_TYPE_VERY_SLOW))); // Ракурс камеры очень медленно
DelayCommand(4.0, AssignCommand(oSelf, SpeakString("Давай говори! Что там случилось?")));
DelayCommand(4.1, AssignCommand(oSelf, ActionPlayAnimation
(ANIMATION_LOOPING_TALK_FORCEFUL, 1.0, 5.0)));
DelayCommand(9.0, AssignCommand(oPC, ActionPlayAnimation
(ANIMATION_FIREFORGET_PAUSE_SCRATCH_HEAD, 0.5)));
DelayCommand(9.0, AssignCommand(oPC, SpeakString("Понимаете… эээ… такое дело…")));
// Сменим режим камеры
DelayCommand(9.5, SetCameraMode(oPC, CAMERA_MODE_STIFF_CHASE_CAMERA));
DelayCommand(15.0, AssignCommand(oSelf, SpeakString("А ну-ка, иди-ка ты отсюда прочь!")));
DelayCommand(15.1, AssignCommand(oSelf, ActionPlayAnimation(ANIMATION_FIREFORGET_SALUTE)));
// Вернем режим камеры сверху вниз
DelayCommand(Time, SetCameraMode(oPC, CAMERA_MODE_TOP_DOWN));
DelayCommand(Time, SetLocalInt(oSelf, "CUTSCENE", 0)); // разблокируем ХБ скрипт
DelayCommand(Time, SetCutsceneMode(oPC, FALSE)); // Режим игры, конец ролика
}
|
Давайте теперь рассмотрим код скрипта. Мы задали три точки и определили героя и персонаж, с которым идет диалог, а также определили для цикла первый объект в области. Это нужно для выбора всех членов команды героя, т.е. его спутников. В цикле определяем является ли объект членом команды, и если да, то чистим ему стек, отправляем на заданную точку и накладываем эффект парализации. Причем, если вы заметили, то делаем это в два этапа. Первый, это наложение эффекта как акция самого НПС, идущая сразу после выполнения прыжка. Второй, это наложение с задержкой, на тот случай если все же акция собьется, такая маленькая подстраховка…
Затем мы присвоили локалку для ролика персонажу, тем самым, блокировав его скрипт слота OnHeartbeat, от нежелательного действия. Эту локалку нужно предварительно прописать, в базовом скрипте ее конечно нет, см. пример выше… Этим мы практически мгновенно блокируем НПС и развязываем себе руки для наложение на НПС нужной анимации в ролике.
Дальше, мы чистим стек героя и отправляем его в нужное место, а вот персонаж мы отправляем с небольшой задержкой. Это нужно для того, чтобы отработали скрипты при завершении диалога. Для старта сценки ролика, по этой же причине, мы также делаем небольшую задержку…
Если сценка будет начинаться с триггера или другого места, несвязанного с исполнением скриптов, способных повлиять на старт ролика, то задержку можно не делать. Например, для триггеров на слот OnEnter нужна просто проверка на то, что действительно ли зашедший является героем, и настало ли время для сценки, а так же нет других помех для старта. Например:
void main()
{
object oPC = GetEnteringObject();
object oSelf = OBJECT_SELF;
object oNPC = GetFirstObjectInArea(oPC);
if(!GetIsPC(oPC)) return; // Если не герой
if(GetIsInCombat(oPC))return; // Если идет бой
while (GetIsObjectValid(oNPC)) // Если в области есть враги
{
if(GetIsEnemy(oPC, oNPC)) return; // Если враги
oNPC = GetNextObjectInArea(oPC);
}
if(GetLocalInt(oPC,"Cutscene_1")== 0) return; // Если нет локалки
if(GetLocalInt(oSelf,"DEACTIVATED")== 1) return; // Блокировка самого триггера
// *** СТАРТ РОЛИКА ***
SetCutsceneMode(oPC, TRUE); // Режим ролика
/* САМ РОЛИК */
SetLocalInt(oSelf,"DEACTIVATED", 1); // Блокировка самого триггера
}
|
В самой сценки нет ничего сложного, просто проставлена анимация и текст для персонажей, а так же мы несколько раз меняем ракурс камеры и ее режим. В завершение ролика мы вернем персонажу значение переменной локалки "CUTSCENE", игровой режим камеры герою (CAMERA_MODE_TOP_DOWN) и завершим ролик. Это сделано через использование общего времени сценки Time, которое мы задали в самом начале скрипта. По истечению этого времени, также закончится эффект парализации спутников. Вот и все…
|