|
"Aurora
Toolset": Анимация NPC
ЧАСТЬ I (продолжение)
КАК СДЕЛАТЬ ?
Приносим извинения за задержку с выходом этой части. С уважением,
Lex & LexxL.
Ну что ж, теперь, когда мы немного знаем о том, как работать
с анимацией, давайте посмотрим на следующих примерах, как это воплотить
в единую систему.
Это - скрипт первого кузнеца. Не буду
объяснять, что такое UserDefined и как с ним работать, это вам должно
быть известно.
void main()
{
int nUser = GetUserDefinedEventNumber();
if(nUser == 1001) //HEARTBEAT
{
if(GetLocalInt(OBJECT_SELF,"BUSY")==0) //смотрим,
а не занят ли наш NPC уже. Если НЕ ЗАНЯТ, то идем ниже
{
SetLocalInt(OBJECT_SELF,"BUSY",1);
// ставим, что NPC занят. Те через 6 сек,
когда скрипт снова запустится, он уже вниз не пойдет
object oBox = GetNearestObjectByTag("BOX_1");
object oAnvil = GetNearestObjectByTag("ANVIL_1");
// определяем объекты, которые нам понадобятся
object oFlame = GetNearestObjectByTag("FLAME_1");
switch (Random(4)+1) //
рандомная переключалка сценариев (см первую часть статьи).
Рандомная это для наглядности, система выбора может быть другой. Мы
это дальше увидим.
{
case 1://
сценарий первый.
ActionMoveToObject(GetNearestObjectByTag("BOX_POINT_1"),FALSE,0.0);
// идем к поинту у ящика
ActionDoCommand(SetFacingPoint(GetPosition(oBox)));
// поворачиваемя в сторону ящика
ActionPlayAnimation(ANIMATION_LOOPING_GET_LOW,1.0,2.0);
// приседаем и открываем
ActionDoCommand(AssignCommand(oBox,ActionPlayAnimation(ANIMATION_PLACEABLE_OPEN)));
// открыли
ActionPlayAnimation(ANIMATION_LOOPING_GET_LOW,1.0,4.0);
// копаемся
ActionPlayAnimation(ANIMATION_FIREFORGET_DRINK);//
пьем что-то (типа он постоянно освежается)
ActionSpeakString("ухх, хорошо!");
// говорим фразу ** смдополнения на тему "Фразы".
ActionPlayAnimation(ANIMATION_LOOPING_GET_LOW,1.0,2.0);
// закрываем ящик
ActionDoCommand(AssignCommand(oBox,ActionPlayAnimation(ANIMATION_PLACEABLE_CLOSE)));
// закрыли
break;
case 2:
/// и так далее, сценарии несложные.
ActionMoveToObject(GetNearestObjectByTag("BOOKS_POINT_1"),FALSE,0.0);
ActionPlayAnimation(ANIMATION_LOOPING_GET_MID,1.0,2.0);
ActionSpeakString("ага, вот они!");
ActionPlayAnimation(ANIMATION_FIREFORGET_READ);
break;
case 3:
ActionMoveToObject(GetNearestObjectByTag("ANVIL_POINT_1"),FALSE,0.0);
ActionDoCommand(SetFacingPoint(GetPosition(oAnvil)));
ActionEquipItem(GetFirstItemInInventory(OBJECT_SELF),INVENTORY_SLOT_RIGHTHAND);
ActionPlayAnimation(ANIMATION_FIREFORGET_STEAL);
ActionPlayAnimation(ANIMATION_LOOPING_TALK_FORCEFUL);
ActionPlayAnimation(ANIMATION_FIREFORGET_PAUSE_SCRATCH_HEAD);
ActionSpeakString("Oh yea");
ActionPlayAnimation(ANIMATION_FIREFORGET_STEAL);
ActionPlayAnimation(ANIMATION_FIREFORGET_STEAL);
ActionPlayAnimation(ANIMATION_LOOPING_TALK_FORCEFUL);
ActionUnequipItem(GetItemInSlot(INVENTORY_SLOT_RIGHTHAND,OBJECT_SELF));
break;
case 4:
ActionMoveToObject(GetNearestObjectByTag("FLAME_POINT_1"),FALSE,0.0);
ActionDoCommand(SetFacingPoint(GetPosition(oFlame)));
ActionPlayAnimation(ANIMATION_LOOPING_GET_LOW,0.5,7.0);
ActionDoCommand(AssignCommand(oFlame,ActionPlayAnimation(ANIMATION_PLACEABLE_ACTIVATE)));
ActionDoCommand(AssignCommand(oFlame,DelayCommand(10.0,PlayAnimation(ANIMATION_PLACEABLE_DEACTIVATE))));
ActionPlayAnimation(ANIMATION_LOOPING_GET_LOW,0.5,4.0);
ActionSpeakString("ooo hot!");
break;
}
ActionDoCommand(SetLocalInt(OBJECT_SELF,"BUSY",0));
// самая важная в скрипте штука, выставление занятости на 0. Если
сделать БЕЗ ActionDoCommand, то она СРАЗУ сделается 0, и нашему NPC
в очередь добавятся след. сценарии. Ничего вроде плохого, но через
минуту на него повесится 10 сценариев, а выполнит он только 2-3. А
что будет через час!!
}
}
}
Те BUSY - единственный параметр системы.
Если 0, то NPC закончил делать что-то, а нового пока нет. Если 1,
то он в выполняет сценарий. Но вот в чем загвоздка: ЕСЛИ ЕГО ПРЕРВАТЬ
РАЗГОВОРОМ, ТО ОН ПОТЕРЯЕТ ВЕСЬ СЮЖЕТ, В ТОМ ЧИСЛЕ И ActionDoCommand(SetLocalInt(OBJECT_SELF,"BUSY",0));,
а значит для ВСЕХ СЛЕДУЮЩИХ ЗАПУСКОВ СКРИПТА ВСЕГДА БУДЕТ ЗАНЯТ!!
Чтобы этого не произошло, на окончание диалога ставим скрипт:
void main()
{
SetLocalInt(OBJECT_SELF,"BUSY",0);
}
Теперь, после разговора он будет СВОБОДЕН
и произойдет выбор следующего сценария! Во-о-о-от, с первым разобрались.
Оттестили, попрерывали.
НО! Он если шел пить, а мы его прервали, то НЕ ОБЯЗАТЕЛЬНО
в следующем сценарии он снова пойдет пить. Он передумал что ли, пока
с игроком разговаривал ?!
Значит нам надо запомнить, что он делал, когда его прервали, и тогда
мы сможем прерванный сценарий начать сначала. Что для этого надо?
Введем еще один параметр - локалку ActionSet (номер сценария).
(чтобы не затягивать, сазу введем еще одну фишку - связь сценариев)
Это - скрипт второго кузнеца.
void main()
{
int nUser = GetUserDefinedEventNumber();
if(nUser == 1001) //HEARTBEAT
{
if (GetLocalInt(OBJECT_SELF,"BUSY")==0)
{
SetLocalInt(OBJECT_SELF,"BUSY",1);
object oBox = GetNearestObjectByTag("BOX_2");
object oAnvil = GetNearestObjectByTag("ANVIL_2");
object oFlame = GetNearestObjectByTag("FLAME_2");
if (GetLocalInt(OBJECT_SELF,"ActionSet")==0) //
ЕСЛИ ПРЕДЫДУЩИЙ СЦЕНАРИЙ БЫЛ ЗАВЕРШЕН, ИЛИ НЕТ ЖЕСТКОГО СЛЕДУЮЩЕГО
СЦЕНАРИЯ (СВЯЗКА)
{
SetLocalInt(OBJECT_SELF,"ActionSet",Random(4)+1); //
то рандомный выбор
}
switch (GetLocalInt(OBJECT_SELF,"ActionSet")) //
по ActionSet выбор сценария
{
case 1: //см.скрипт 1-го кузнеца
ActionDoCommand(SetLocalInt(OBJECT_SELF,"ActionSet",0)); //нет
жестко заданного следующего сценария
break;
case 2:///см.скрипт 1-го кузнеца
ActionDoCommand(SetLocalInt(OBJECT_SELF,"ActionSet",0)); //нет
жестко заданного следующего сценария
break;
case 3://см.скрипт 1-го кузнеца
ActionDoCommand(SetLocalInt(OBJECT_SELF,"ActionSet",0)); //нет
жестко заданного следующего сценария
break;
case 4: // новый сценарий
ActionMoveToObject(GetNearestObjectByTag("COAL_POINT_2"),FALSE,0.0);
ActionPlayAnimation(ANIMATION_FIREFORGET_PAUSE_SCRATCH_HEAD);
ActionPlayAnimation(ANIMATION_LOOPING_GET_LOW,0.5,7.0);
ActionSpeakString("уголь плохой");
ActionDoCommand(SetLocalInt(OBJECT_SELF,"ActionSet",5));
// ЖЕСТКИЙ! Выбор следующего сценария. Т.е. после 4 ВСЕГДА будет 5
, НО! при этом сам по себе 5 не будет! (это просто я так сделал)
break;
case 5://(сценарий, без изменений)
ActionDoCommand(SetLocalInt(OBJECT_SELF,"ActionSet",0)); //нет
жестко заданного следующего сценария
break;
}
ActionDoCommand(SetLocalInt(OBJECT_SELF,"BUSY",0));
}
}
}
Теперь наша ситуация такова:
BUSY |
ActionSet |
состояние NPC ? |
0 |
не 0 |
значит NPC был прерван диалогом или имеется ЖЕСТКАЯ связка
сценариев, т.е. при СЛЕДУЮЩЕМ ЗАПУСКЕ скрипта будет выбран прерванный
сценарий с номером ActionSet |
0 |
0 |
Предыдущий сценарий выполнен и NPC ждет нового Random СЦЕНАРИЯ
|
1 |
НЕ 0 |
NPC ЗАНЯТ выполнением текущего сценария |
1 |
0 |
ТАКОГО НЕ МОЖЕТ БЫТЬ! |
Теперь наш NPC не только запоминает, что же он хотел делать, когда
игроку вздумалось с ним поболтать, но и МОЖЕТ ДЕЛАТЬ СВЯЗКИ СЦЕНАРИЕВ.
(добавьте еще то, что можно дополнительных условий туда напихать,
и тогда сами связки будут НЕ ЖЕСТКИЕ а динамические)
Не устали?? Тогда идем дальше - интерактивный
кузнец (третий)
void main()
{
int nUser = GetUserDefinedEventNumber();
if(nUser == 1001) //HEARTBEAT
{
if (GetLocalInt(OBJECT_SELF,"BUSY")==0)
{
SetLocalInt(OBJECT_SELF,"BUSY",1);
object oBox = GetNearestObjectByTag("BOX_3");
object oAnvil = GetNearestObjectByTag("ANVIL_3");
object oFlame = GetNearestObjectByTag("FLAME_3");
object oLocals = GetNearestObjectByTag("LOCALS_STORE"); //
просто хранитель локалок. Я редко храню на NPC внешние локалки
switch (GetLocalInt(oLocals,"PC_sword")) //
а вот и нововведение, в зависимости от того, какой "PC_sword" выбирается
след сценарий. "PC_sword" своего рода счетчик. В нем хранится номер
элемента сюжета, который сейчас будут делать.
{
case 1: SetLocalInt(OBJECT_SELF,"ActionSet",4);break;
case 2: SetLocalInt(OBJECT_SELF,"ActionSet",4);break;
case 3: SetLocalInt(OBJECT_SELF,"ActionSet",5);break;
case 4: SetLocalInt(OBJECT_SELF,"ActionSet",3);break;
case 5: SetLocalInt(OBJECT_SELF,"ActionSet",3);break;
case 6: SetLocalInt(OBJECT_SELF,"ActionSet",3);break;
case 7: SetLocalInt(OBJECT_SELF,"ActionSet",1);break;
case 8: SetLocalInt(OBJECT_SELF,"ActionSet",3);break;
case 9: SetLocalInt(OBJECT_SELF,"ActionSet",3);break;
case 10: SetLocalInt(OBJECT_SELF,"ActionSet",6);break;
}
if (GetLocalInt(OBJECT_SELF,"ActionSet")==0) //
если по PC_sword ничего не выбрали (те PC_sword ==0) то - рандомный
выбор сценария
{
SetLocalInt(OBJECT_SELF,"ActionSet",Random(4)+1);
}
switch (GetLocalInt(OBJECT_SELF,"ActionSet"))
{
case 1:(см.скрипт 2-го кузнеца)
ActionDoCommand(SetLocalInt(OBJECT_SELF,"ActionSet",0));
break;
case 2:(см.скрипт 2-го кузнеца)
ActionDoCommand(SetLocalInt(OBJECT_SELF,"ActionSet",0));
break;
case 3: (см.скрипт 2-го кузнеца)
ActionDoCommand(SetLocalInt(OBJECT_SELF,"ActionSet",0));
break;
case 4:(см.скрипт 2-го кузнеца)
ActionDoCommand(SetLocalInt(OBJECT_SELF,"ActionSet",5));
break;
case 5:(см.скрипт 2-го кузнеца)
ActionDoCommand(SetLocalInt(OBJECT_SELF,"ActionSet",0));
break;
case 6: // этот сценарий выполняется,
когда меч готов. Кузнец идет у игроку и отдает его. (если же игрок
далеко, то далее идет рандомный выбор сценариев, но как только мы
заговорим с ним, сразу отдаст меч).
ActionDoCommand(SetFacingPoint(GetPosition(GetFirstPC())));
if (GetObjectSeen(GetFirstPC(),OBJECT_SELF))
{
ActionMoveToObject(GetFirstPC(),FALSE,0.0);
ActionStartConversation(GetFirstPC(),"giving_sword");
}
ActionDoCommand(SetLocalInt(OBJECT_SELF,"ActionSet",0));
break;
}
if((GetLocalInt(oLocals,"PC_sword")!=0)&&(GetLocalInt(oLocals,"PC_sword")<=10))
ActionDoCommand(SetLocalInt(oLocals,"PC_sword",GetLocalInt(oLocals,"PC_sword")+1));
/// переключалка PC_sword. Выполнили
сценарий, PC_sword+1. Те теперь по сюжету будет сценарий соответствующий
PC_sword+1 значению.
ActionDoCommand(SetLocalInt(OBJECT_SELF,"BUSY",0));
}
}
}
Есть в модуле и четвертый кузнец. Но он не круче 3-го, просто у него
есть схема перебора сценариев без повторов.
В чем его фишка? А в том, что выборка сценариев такая, что он никогда
не сделает действие дважды подряд.
Четвертый кузнец, который уходит спать...
void main()
{
int nUser = GetUserDefinedEventNumber();
if(nUser == 1001) //HEARTBEAT
{
if(GetLocalInt(OBJECT_SELF,"BUSY")==0)
{
SetLocalInt(OBJECT_SELF,"BUSY",1);
int NewSet = Random(4)+1; /// наше
следующее действие
object oBox = GetNearestObjectByTag("BOX_4");
object oAnvil = GetNearestObjectByTag("ANVIL_4");
object oFlame = GetNearestObjectByTag("FLAME_4");
if(GetLocalInt(OBJECT_SELF,"ActionSet")==0)
SetLocalInt(OBJECT_SELF,"ActionSet",NewSet); ///
если ActionSet==0 значит, что скрипт запущен первый раз, так-как больше
НИГДЕ НЕТ ОБНУЛЕНИЯ. Так как первый раз, то надо что-то записать в
него.
if (GetLocalInt(OBJECT_SELF,"ActionSet")==4) ///
создание связки 4-5, теперь она здесь.
{
SetLocalInt(OBJECT_SELF,"ActionSet",5);
}
else // для любого, кроме 4 происходит
{
while (GetLocalInt(OBJECT_SELF,"ActionSet")==NewSet) //
выбор но при этом если NewSet (след действие) == ActionSet (только-что
выполненое действие)
{
NewSet = Random(4)+1; // то происходит
переприсвоение NewSet
}
SetLocalInt(OBJECT_SELF,"ActionSet",NewSet);
}
if (GetTimeHour()==18) SetLocalInt(OBJECT_SELF,"ActionSet",6); //
дополнит фишка: идет спать в 18 часов. По другому ActionSet НИКОГДА
НЕ СТАНЕТ РАВНЫМ 6
switch (GetLocalInt(OBJECT_SELF,"ActionSet"))
{
case 1:см.скрипт 3-го кузнеца
case 2:см.скрипт 3-го кузнеца
case 3:см.скрипт 3-го кузнеца
case 4:см.скрипт 3-го кузнеца
case 5:см.скрипт 3-го кузнеца
case 6: // идет спать
DelayCommand(1.0,SetCommandable(FALSE));
ActionPlayAnimation(ANIMATION_FIREFORGET_PAUSE_BORED,1.0);
ActionSpeakString("Ohh.. i'm so tired, i want to sleep");
ActionMoveToObject(GetNearestObjectByTag("SMITH4_CORIDOR"));
ActionJumpToObject(GetNearestObjectByTag("CORIDOR_SMITH4"));
ActionMoveToObject(GetNearestObjectByTag("CORIDOR_POINT_4"),FALSE,0.0);
ActionMoveToObject(GetNearestObjectByTag("SLEEP_POINT_4"),FALSE,0.0);
ActionSpeakString("Ohh.. i'm so tired, than can't move");
ActionPlayAnimation(ANIMATION_LOOPING_DEAD_FRONT,0.1);
ActionDoCommand(ApplyEffectToObject(DURATION_TYPE_INSTANT,EffectSleep(),OBJECT_SELF));
break;
}
ActionDoCommand(SetLocalInt(OBJECT_SELF,"BUSY",0));
}
}
}
Так, анимация, это хорошо, но "голая" она не катит. Что надо сделать,
чтобы все смотрелось лучше и более живо?
... Правильно! Нужны ЗВУКИ И ФРАЗЫ.
ЗВУКИ
Тут все плохо, так как звуков много, а скриптер один. Есть два разных
способа работы с ними.
1. посредством PlaySound(); мы можем
проиграть однородный разовый звук.
Например, при ударе молотком по наковальне должен проигрываться звук
удара.
2. посредством SoundObjectPlay();
- это позволяет проигрывать звук, который РАЗМЕЩЕН в модуле.
Например, кинули угля в печь и запустили звук громкого горения. (Тут
можно сделать и по-другому: с помощью SoundObjectSetVolume();
увеличить громкость того звука, что уже играл.)
Работа со звуком, как таковая, очень проста, но вот подборка звука
может занять много времени.
ФРАЗЫ
Тут все гораздо проще. Для того, чтобы немного "оживить" NPC (а то
он прям молчун какой-то) можно поставить фразы на разные этапы его
работы.
Это, как и звуки, ОЧЕНЬ СИЛЬНО украшает ролик, и делает NPC
более реалистичным. Вы можете, как в примерах, сделать так, чтобы
он каждый раз в одном и том же месте говорил одну и ту же фразу, но
это будет не очень привлекательно
(в примерах это сделано с целью большей наглядности скрипта анимации
в целом). Мы пользуемся следующей схемой фраз: перед скриптом описывается
одна или несколько (по количеству NPC задействованных в сюжете) функций
типа типа Phrase();.
void Phrase(int ActionSet)
{
string s="";
switch(i);
{
case 1:
switch(Random(10))
{
case 0: s="фраза 1 на ActionSet ==1"break;
case 1: s="фраза 2 на ActionSet ==1"break;
…………
case 5: s="фраза 5 на ActionSet ==1"break;
}
break;
case 2:
switch(Random(10))
{
case 0: s="фраза 1 на ActionSet ==2"break;
case 1: s="фраза 2 на ActionSet ==2"break;
…………
case 5: s="фраза 5 на ActionSet ==2"break;
}
break;
………….
}
if (s!="") SpeakString(s);
}
Для чего все это?? Давайте рассмотрим по порядку.
- мы вносим в функцию параметр ActionSet,
так как при проигрывании разных сценариев NPC должен говорить разные
слова. (а не так, что стоя у книжной полки сказать "Как сегодня ковка
хорошо пошла!")
Random(10) нам нужен для того, чтобы
при проигрывании одного и того же сценария NPC говорил разные фразы.
(тут по вашему желанию, можете и не делать так). Почему Random(10)
а case только до 5? Все просто,
если Random(10)>5 то NPC ничего
не скажет, что тоже есть элемент разнообразия, как и разные фразы.
Что можно сделать еще? Например проверить, если игрок в локации и
близко к NPC (те смотрит на NPC), то можно сделать отдельный блок
фраз на эту тему, типа "Ну чего, не видел кузнеца за работой?".
Все эти скрипты достаточно просты, но имеют определенные недостатки.
Вы заметили, что иногда молотне убирается из рук или если кузнеца
прервать во время ковки, то он так и останется в руке. В принципе,
если на начало диалога убрать молот, если он есть, то проблема решается.
Но это уже не относится к данной статье. Естественно, эти скрипты
требуют доводки, подгонки и так далее. НО! Если вы хоть что-то полезное
для себя вынесли из статьи, то мы очень рады! Если у вас есть вопросы,
или вы обнаружили ошибки или неточности, то пишите на Lex-WRG@yandex.ru
или оставляйте сообщение на форуме сайта www.realms.ru (это более
предпочтительно, так как и другие смогут это прочитать) в разделе
"Город Мастеров"в теме "Оживление NPC". Мы там периодически бываем
и Lex или LexxL охотно вам ответят.
Продолжение НЕ следует...
Пока
все ... >>>
Внимание!
Данный
текст является интеллектуальной собственностью. Любое цитирование
материалов допустимо только со ссылкой на наш сайт!
|