Скрипты в Skyrim Construction Set

*Это руководство не претендует на полноту или абсолютную достоверность.

Скрипты и Skyrim Construction Set

Часть 1: Зачем это нужно

Проводя время в играх серии Elder Scrolls (или Fаllout 3) вы, наверное, замечали, как много объектов игрового мира взаимодействуют между собой. Зайдите в любой город и убедитесь в этом: по улицам спешат по своим делам горожане, у кузницы кто-то кует оружие, стражники отпускают колкие замечания в ваш адрес, нищие просят милостыню, а торговцы расхваливают свой товар. Вот вы попытались залезть в чужой карман -  люди мгновенно среагируют, пытаясь вас остановить. Как же получилось, что в огромном мире Тамриэля сотни персонажей имеют распорядок дня, взаимодействуют между собой, дают игроку поручения, сражаются, общаются и с удивительным упорством изображают живых людей? Как получилось, что в Скайриме вы можете после убийства волка содрать с него шкуру, обработать на станке и тут же, в кузнице, выковать новый доспех, переплавив руду, что добыли недавно в шахте? Кого вы должны винить, когда в темном подземелье в очередной раз лезвия механизма-ловушки разрубают на куски вашего персонажа? Как вы уже догадались, виной всему скрипты.

Скрипты в Elder Scrolls: Skyrim  - это шестеренки в механизме, которые делают из игры единое целое. Это цементный раствор, скрепляющий кирпичи его основания. Большинство взаимодействий, которые вы видите в игре - результат работы скриптов. Вы можете добавить в мир Скайрима миллионы новых объектов (оружие и броню, анимацию и магические эффекты, особняки и замки), но они будут "мертвы", пока скрипты не сделают свое дело. Вы можете отредактировать любой объект в Скайриме, но если вы хотите привнести что-то новое, скорее всего, вам потребуются скрипты. Вы можете не догадываться об их работе, пока не встретитесь с ними "в лоб". Задача данного руководства - попытаться показать вам, как работают скрипты и научить создавать простейшие из них.

Если вы изучали программирование (и в особенности, объектно-ориентированное), написание скриптов не должно вызвать у вас затруднений. С другой стороны, тем,  кто программированием не занимался, изучение скиптов поможет в нем разобраться, так как скрипты оперируют в большинстве случаев  "реальными объектами" игры (таким образом, результат можно "пощупать" в игре, что, по-моему, упрощает понимание принципов их работы).

2. Что такое скрипты и с чем их едят

Для написания скриптов в игре Skyrim используется скриптовый язык Papyrus, который по большому счету является объектно-ориентированным языком. Скрипты Papyrus представляют собой файлы с расширением .psc, которые могут быть отредактированы любым текстовым редактором. Однако, для того, чтобы игра их обрабатывала, скрипты необходимо скомпилировать. В общем случае, под процессом компиляции подразумевают процесс перевода программы с языка программирования на язык машинных команд. После компиляции файл скрипта приобретает формат .pex.

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

  •  книжная полка, это ОБЪЕКТ, над которым вы совершаете определенное действие;
  •  факт того, что книжная полка сломалась - это СОБЫТИЕ, которое вы отслеживали;
  •  инструменты - это ФУНКЦИИ, которыми вы оперируете;
  •  гвозди и шурупы - это АРГУМЕНТЫ этих функций или ПЕРЕМЕННЫЕ;
  •  также книжная полка обладает определенными СВОЙСТВАМИ (например, размер);

Эта простая аналогия, которая показывает, как объекты, которыми оперирует скрипт, взаимодействуют между собой.

Разберем более подробно данные объекты.

  • Переменные (Variable) - это хранилище для данных. Т.е. задача переменных - запоминать изменяющиеся в процессе работы скрипта данные. Каждая переменная должна соответствовать определенному типу данных. Если мы начнем оперировать с переменной одного типа, как с переменной другого типа, это будет то же самое, как если бы мы забивали шурупы молотком. Вы должны точно определить тип переменной, которую собрались использовать. Существуют переменные,  доступные только внутри скрипта, а также глобальные пременные (Globalvariables), для взаимодействия скриптов между собой (например, переменная, хранящая прошедшее игровое время)
  • Функции (Function), это инструменты, с помощью которых вы можете каким-то образом взаимодействовать с объектами. Есть множество "стандартных" функций, доступных в редакторе Creation kit, которые вы можете использовать по своему усмотрению. Однако вы также можете создавать и свои функции. Обычно функции привязаны к определенному объекту. Например, у нас есть инструмент молоток (функция). Нам нужно сообщить этому молотку, какой предмет мы собираемся ремонтировать (книжная полка в данном случае - объект, над которым производит действия данная функция), а также какие гвозди (переменные) забивать и куда (дополнительные параметры). Функция может возвращать определенный параметр,  как результат своего действия.

В псевдокоде это будет выглядеть как:

Результат = Человек.Молоток (книжная-полка, гвозди)

Здесь человек - это объект, которому принадлежит функция молоток(), а книжная полка и гвозди - переменные, которые мы сообщаем данной функции. В переменную Результат запишется, получилось у нас отремонтировать полку или нет. Функции - наиболее мощный инструмент языка Papyrus.

  • Объект (Object) - это нечто, обладающее состоянием, поведением и идентичностью; условно, "контейнер", содержащий в себе переменные, функции, скрипты, а также отвечающий на определенные события  и имеющий определенный идентификационный номер (ссылку ReferenceID) . В Скайриме объектами являются книги, двери, анимации, звуки, персонажи, обстановка интерьера и т.п., т.е. все вещи, которые нас окружают в игре. Если вы откроете редактор CreationKit, в окне Objects вы увидите объекты, с большинством из которых могут работать скрипты. *Строго говоря,  в игре существует триединство 3D модели, скрипта этой модели и отнесенных к этой модели свойств.
  • События (Event) - когда в скрипте мы объявляем событие, это означает для игры, что мы хотим отследить определенное действие игрока (или другого объекта), если оно совершается над данным объектом. Определенным объектам соответствуют определенные события. Так, к примеру, бесполезно отслеживать событие для книжной полки: "пойдет ли она на работу", так как в принципе книжная полка сделать этого не может.
  • Свойства (Properties) -  это новая "фишка" Скайрима, недоступная в прошлых версиях игры. По сути, это те же переменные. Отличие заключается в том, что они доступны "извне", т.е. другие скрипты могут изменять их значения. Для каждого конкретного объекта Свойства можно задать вручную непосредственно в редакторе.

3. Три страшных слова. Наследование, инкапсуляция, абстракция

Чтобы разобраться, как функционируют скрипты Papyrus, придется углубиться в объектно-ориентированное программирование.

Общие понятия:

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

Здесь под классом подразумевается еще несуществующий объект (программный код), а под объектом – конкретно существующий объект, реализованный в пространстве игры.

Чтобы понять, как это работает и зачем нужно, я расскажу вам сказку.
 

«Жил-был в одной стране один мудрый король. И было у этого короля три сына. Хорошие выросли сыновья: красивые, высокие, статные, все в мать. Умом их тоже Бог не обделил, ведь отец у них был человеком неглупым. Первый сын был храбрый, словно лев. Второй сын – сильный, словно бык. Третий сын – решал в уме интегро-дифференциальные уравнения. Поехали эти сыновья в чужие страны счастье себе добывать, но на беду война на родине случилась. Вызывает их отец к себе и говорит: «Покажите, мои сыновья, себя в бою и защитите Родину Святую!”. Собрались сыновья с духом, облачились в доспехи крепкие и показали, где раки зимуют иноземным захватчикам. Тут и первый сын доказал свои храбрость превеликую, второй сын доказал свою силушку богатырскую, а третий сын доказал, что умеет решать в уме интегро-дифференциальные уравнения…».

Сказка ложь, да в ней намек. Король, в данном случае – родительский объект. Сыновья повторяют свойства своего отца и своей матери (умные и красивые), но, в тоже время, обладают новыми свойствами, и могут их использовать. Храбрость, сила и способность решать в уме интегро-дифференциальные уравнения – это функции дочерних объектов, которые мы можем «вызвать» для своих нужд.

Это иерархическая структура, благодаря которой свойства старых объектов могут быть использованы в новых. В структуре объектов Скайрима объект Form – занимает высшую ступень. На основе него созданы все остальные объекты. В Объекте Form объявлены определенные функции и события. Все эти события также доступны, к примеру, в скриптах объектов (ObjectReference), которые являются дочерними для Form. Ниже ступенью идут скрипты Актеров (ActorScripts), «расширяющие» возможности ObjectReference. Когда мы создаем какой-либо скрипт, мы просто «расширяем» возможности уже имеющихся скриптов и можем использовать все функции, которые доступны в родительских объектах. 

Таким образом,  в скиптах  ActorScripts, мы можем использовать как функции Form, так и функции ObjectReference.  Грубо говоря, нам не нужно даже знать, как работает объект Form для использования его функций в объекте ActorScripts.

Объектно-ориентированное программирование - это методология программирования, основанная на представлении программы в виде совокупности объектов, каждый из которых является объектом определенного класса, а классы образуют иерархию наследования.

4. От теории – к практике

Мы познакомились с основами объектно - ориентированного языка и узнали из чего состоит скрипт.

Теперь самое время приступить к практике.
Рассмотрим пример скрипта, на основании которого я более подробно расскажу о непосредственной реализации скриптов игре.

Scriptname MyFirstScript extends ObjectReference
MiscObject property MyItem auto
Event OnActivate(ObjectReference akActionRef)
akActionRef.additem(MyItem, 1)
endevent

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

1) Первая строка – объявление скрипта

Scriptname MyFirstScript extends ObjectReference

Это объявление имени скрипта и указание на родительский объект. В данном случае после ключевого слова Scriptname мы указываем имя скрипта  - MyFirstScript. Это имя будет использоваться для его идентификации.
Ключевое слово extends служит для указания родительского скрипта. В данном случае мы указываем в качестве родителя ObjectReference, «расширяя» этим его свойства. Мы можем указать и другого родителя (например, Form, Actor, Activator), но в данном случае мы хотим приобрести свойства именно ObjectReference.

После этого нам станут доступны функции, описанные в родительском объекте, например,

  • Function Activate (ObjectReference akActivator)
  • Function AddItem (Form akItemToAdd, int aiCount, bool abSilent)
  • Function Disable (bool abFadeOut), которые указаны в ObjectReference.

*О функциях подробнее я расскажу позже.

2) Объявление переменных

MiscObject property MyItem auto

После указания имени скрипта мы должны «объявить» переменные, т.е. дать скрипту контейнеры для хранения информации, четко определив их ИМЯ и ТИП.

MiscObject – это тип переменной, в которой может храниться MiscItem. Если вы откроете Creation Kit, вы можете найти подобные объекты в окне Objects, эти объекты разбросаны повсюду. К ним относятся корзины, слитки, куски кожи, тарелки и кувшины и т.п. Один из них мы добавим игроку, благодаря этому скрипту.
Остановимся подробнее на типах данных.

В языке Papyrus существуют следующие типы данных:

Literal:

  • integer: целочисленный тип данных.

Пример:
Int count
Int Number
Int BigNum = 100
Int ключевое слово для определения переменной типа integer.
В последнем случае я одновременно объявил переменную и присвоил ей значение 100 с помощью оператора «=».

  • float:  число с плавающей точкой, например 1.567 или  768.0964:

Float ABC
Float MyCount = 3.89 * 45.09

  • boolean: могут иметь два значения True (правда) или False (ложь):

Bool MyBoolean = true

  • string: строковый тип данных. Набор символов, например: «Привет, мир!»

String MyString

  • none: «пустое» значение (null);

Переменные этих типов можно объявлять как внутри скрипта, так и внутри функций. При этом если в скрипте имеются две функции с переменными, имеющими одинаковые имена,  они не будут конфликтовать.
По умолчанию всем переменным присваиваются их Default значения (0 для float и integer, false для Boolean).
Ключевое слово property необходимо для обозначения этой переменной как Свойства. Т.е. данная переменная будет видна другим скриптам. Вы сможете изменить ее непосредственно в редакторе.
auto  –  ключевое слово для автоматического определения свойства. Если его указать, Papyrus автоматически обрабатывает property. В большинстве случаев этого достаточно.

Например, если я хочу объявить переменную с именем MyInteger типа integer как свойство (property) и тут же присвоить ей значение 253, мне нужно прописать:

Int property MyInteger  = 253 Auto

Каждый объект в Скайриме обладает собственным ID Reference, или по-другому ссылкой, идентификационным номером. Чтобы хранить этот ID и получать ссылку на этот объект мы можем использовать тип ObjectReference.

ObjectReference property MyObject Auto

3) Event OnActivate(ObjectReference akActionRef)

Endevent

Между ключевыми словами Event и Endevent, заключена обработка события OnActivate. Это событие расширяет базовое событие объекта ObjectReference. Для объектов ObjectReference предусмотрены события:
Event OnContainerChanged (ObjectReference akNewContainer, ObjectReference akOldContainer) (Событие при смене контейнера, например, когда предмет из сундука попадает в инвентарь игроку.)
Event OnEquipped (Actor akActor)(Событие при экипировке объекта)

Event OnHit (ObjectReference akAggressor, Form akSource, Projectile akProjectile, bool abPowerAttack, bool abSneakAttack, bool abBashAttack, bool abHitBlocked) (Событие при ударе объекта)
Event OnLoad() (Событие при загрузке объекта в память) и многие другие.

OnActivate(ObjectReference akActionRef)
Событие OnActivate происходит, когда кто-либо пытается активировать объект. Чтобы узнать, кто именно это делает (им может быть и не игрок), предусмотрена переменная типа ObjectReference. Таким образом, если событие произошло, «на руках» у нас будет переменная с именем akActionRef, которая содержит ссылку на объект. И для чего же нам это нужно?

4) akActionRef.additem(MyItem, 1)

Мы можем оперировать с данными переменными как с базовыми объектами. Здесь реализована функция:
Function Additem (Form akItemToAdd, int aiCount, bool abSilent)

Вы должны сообщить ей две переменные – одна (akItemToAdd) указывает на предмет, который вы хотите передать, другая (aiCount) на кол-во этих предметов. Эта функция базовая для объекта ObjectReference. Переменная akActionRef содержит ссылку на ObjectReference, поэтому мы можем вызвать функцию additem (), которая была объявлена в ObjectReference.

Для начала мы пишем переменную akActionRef, после точки сообщаем, какую функцию необходимо вызвать, в скобках указываем параметры для функции. Если вы не укажете параметры, они будут установлены в значения Default.  Функция additem () имеет третью переменную abSilent, имеющую тип Boolean, которая устанавливается атоматически в False и отвечает за то, появится ли для игрока сообщение о добавлении предмета.

Таким образом, данный скрипт расширяет возможности ObjectReference, реагирует на событие активации и добавляет в инвентарь персонажу, активировавшего его, предмет MiscItem.

До этого момента вам не требовалось запускать редактор Creation Kit. Я надеюсь, вы имеете базовые навыки работы с ним.

Открываем редактор Creation Kit и размещаем в игре активатор.

 Я выбрал рычаг и поместил его на улице Ривервуда. Теперь мы должны отредактировать скрипт, который привязан к этому объекту. Для этого заходим в свойства объекта и ищем вкладку Scripts (самая правая вкладка). Скорее всего, к активатору уже будет привязан скрипт, но нам он не нужен. Выделяем скрипт и нажимаем кнопку Remove. Теперь мы можем добавить свой скрипт. Нажимаем кнопку add.

 Должно открыться окно PapyrusScriptManager( оно также доступно в Gameplay-> PapyrusScriptManager). Здесь вы можете найти любой скрипт, присутствующий в игре. Нам нужно создать новый. Находим [New Script]->ok. Появится окно AddNewScript. Вводим в поле Name имя нашего скрипта. Остальные поля пока трогать не будем. Жмем ок.

Добавленный скрипт отобразится в окне выбора. Чтобы отредактировать этот скрипт нажмем на него правой кнопкой мыши и выберем EditSource.

Откроется окно текстового редактора. Добрый дядя Papyrus уже написал для нас имя скрипта и то, что он расширяет. Нам остается только ввести обработку события и переменные.

MiscObject property MyItem auto
Event OnActivate(ObjectReference akActionRef)
akActionRef.additem(MyItem, 1)
endevent

edit.png

Все, теперь можно этот скрипт скомпилировать. Жмем File->save, или Ctrl+S. Произойдет автоматическое сохранение и компиляция.
Но тут…

Упс! Ошибка. Вместо MiscObject у меня написано MiscItem. Ошибки неизбежны. Даже если скрипт компилируется, это не значит, что он будет работать правильно! Компилятор не сможет обнаружить ваши логические ошибки. В разделе Compiler Output, компилятор сообщает вам о найденных ошибках. Если с английским языком у вас все в порядке, вам не составит труда их выловить. Смиритесь, большую часть времени вы будете проводить в раздумьях типа, «какого &%№!  эта %@?№  не работает?».

Если у вас все скомпилировалось и ошибок вы не допустили, нам остается сделать последний шаг и установить значение MyItem. Для этого опять же правой кнопкой мыши кликаем на скрипт и выбираем EditProperties. Слева будут указаны свойства данного скрипта (Property Name).  Выбираем MyItem. Нажимаем на кнопку Edit Value и выбираем из списка Pick Object интересующий нас предмет (это те же предметы, что и в окне Objects).  Я выбрал GemAmethyst. Скрипт готов.
 
Сохраняемся, запускаем наш плагин и тестируем непосредственно в игре.

Для того чтобы переместиться в игре в интересующую нас локацию можно использовать консольную команду coc Имя-локации. Например команда coc Riverwood переместит игрока в центр Ривервуда.

Часть 2: Операторы и функции

5. Мы писали, мы писали, наши пальчики устали…

Как вы знаете, файлы скриптов языка Papyrus имеют расширение .psc. Их можно отредактировать в любом текстовом редакторе. Чтобы сделать процесс создания скриптов более простым и оградить вас от появления глупых ошибок, я расскажу о некоторых вещах, призванных привнести удобства в наш нелегкий труд. Редактирование или создание больших по объему скриптов требует определенной доли усидчивости и внимания. Разбираться в скрипте в том же блокноте подчас неудобно. Когда требуется выловить мелкую ошибку, стена текста, состоящая из функций и условий, большинство из которых писал вовсе не ты, работе не способствует. Чтобы облегчить работу начинающего скриптера Bethesda сделала специальный пакет для текстового редактора Sublime Text. Вы можете скачать этот простой и полезный редактор с официального сайта www.sublimetext.com, а затем установить дополнением к нему ряд сниппетов, которые используют функции автозаполнения часто используемых форм. Это позволяет не зацикливаться на синтаксических ошибках, нередко ставящих в тупик своей непредсказуемой глупостью. Окно Sublime Text выглядит следующим образом.

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

Подробнее об этом редакторе вы можете узнать на официальной Wiki по Creation Kit.

Наш пример состоит из семи строк. В этом скрипте всего две переменные, одно событие, и одна вызываемая функция. Представьте, что переменных стало в десять раз больше. Вы сможете в точности запомнить, какая переменная к чему относится, для выполнения какой функции была предназначена? Что, если у вас несколько десятков функций? В состоянии ли вы запомнить в точности триста или четыреста строк скрипта? Я думаю, через неделю, вы уже и не вспомните, для чего вам нужна была функция, созданная под названием AFDS_01_G(). Вы также должны понимать, что, возможно, ваш скрипт будет полезен людям. Но если вы хоть чуть-чуть разбираетесь в его структуре, то  другому человеку последовательность ваших действий в скрипте может быть абсолютно непонятна. Чтобы избежать недоразумений, в тексте скрипта любой желающий может разместить комментарии. Комментарии никак не влияют на работу скрипта и не отображаются вне его. Для размещения комментариев используйте «;» или «{» «}», т. е. точку с запятой или фигурные скобки. Например:

Event OnLoad()
;это мои комментарии после точки с запятой
Endevent
{и это тоже комментарии, но уже внутри фигурных скобок}

В  окне PapyrusScriptManager редактора Creation kit вы можете просмотреть скрипты, написанные непосредственно разработчиками. Практически все они прокомментированы тем или иным образом. Если вы знаете английский язык, то благодаря предусмотрительности скриптеров  компании Bethesda вы можете разобрать скрипт «по косточкам» и использовать его для написания своего собственного.

6.Операторы и условия

6.1 Операторы

Для присваивания переменным определенных значений и совершения над ними всем известных арифметических операций, в скриптовом языке Papyrus используются операторы: «=» , «+», «-», «*», «/», «%».
Например:

MyCount01 = Myfloat01*23.5+15/Myfloat02

здесь MyCount01 – переменная типа float, которой мы присваиваем значение (Myfloat01*23.5+15/Myfloat02). Myfloat01и Myfloat02– переменные, над которыми мы производим математические операции. Следует понимать, что в большинстве случаев мы не можем присвоить переменной одного типа значение переменной другого типа. Например:

Int MyCount01 
float MyFloat01
MyFloat01= 45/84*2.34
MyCount01 = MyFloat01

Данный скрипт, несмотря на то, что он кажется правильным, выдаст ошибку несоответствия: type mismatch while assigning to a int (cast missing or types unrelated). Мы попытались переменной (целочисленной) типа integer присвоить значение (1,2534) типа float.

Если мы перепишем скрипт так:

float MyCount01 
float MyFloat01
MyFloat01= 45/84*2.34
MyCount01 = MyFloat01

Ошибки не будет. Чтобы избежать подобных ошибок, можно также использовать оператор приведения к типу: «as».

Int MyCount01 
float MyFloat01
MyFloat01= 45/84*2.34
MyCount01 = (MyFloat01 int)

Здесь мы сообщаем, что хотели бы видеть переменную MyFloat01 как переменную типа integer. Операцию сложения можно также производить над строковыми переменными:

String MyString
MyString = “Привет,” +”Мир”+”!”
В переменную MyString запишется: “Привет, Мир!”

6.2 Особое место занимают переменные булевого типа (Boolean)

Благодаря ним мы можем производить логические операции. Они могут принимать только два значения: true (правда, 1) и false (ложь, 0). Например:

Bool myboolean
Bool truth
Truth=false
Myboolean=true

Мы можем присваивать этим переменным значения 1 или 0.

Truth=0
Myboolean=1

Результат будет тот же самый. Эти переменные необходимы для проверки условий совершения действий. Для них предусмотрены операции:

  • ! - отрицание
  • == - эквивалентность
  • != - неравно
  • > - больше
  • < - меньше
  • >= - больше или равно
  • <= - меньше или равно
  • ||  - логическое ИЛИ
  • && - логическое И

Например:

b = !a
b правда, если а – ложь

b  =  a == c
b правда, если а эквивалентно с

b = a != c
b правда, если а не равно с

b = a > c
b правда, если а больше с

b  = a&&c
b правда, если а И с правда

b=a||c
b правда, если а правда ИЛИ с правда

Для работы с подобными переменными используется оператор условия if-then-else (если-то-иначе).
Рассмотрим принцип действия этого оператора на нашем первом скрипте.

Scriptname MyFirstScript extends ObjectReference
MiscObject property MyItem auto
Event OnActivate(ObjectReference akActionRef)
akActionRef.additem(MyItem, 1)
endevent

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

Если им воспользовался игрок – действие в блоке OnActivate можно совершить. Если же им воспользовался кто-то другой – никаких указаний скрипту мы посылать не будем. Таким образом, между ключевыми словами Event и endevent мы должны поместить блок:

If ; начало блока условия
Endif ;окончание блока условия

Благодаря которому осуществляется проверка условия. Грубо говоря, этот блок можно назвать функцией, которая возвращает значения правда или ложь. У нас есть ссылка (akActionRef ) на объект, и мы должны проверить, соответствует ли эта ссылка игроку (player).

Для получения ссылки на игрока воспользуемся функцией getplayer(), которая содержится в скриптах объекта Game. Мы можем ее вызвать для объекта ObjectReference: game.getplayer(). Как это будет выглядить в скрипте:

Scriptname MyFirstScript extends ObjectReference
MiscObject property MyItem auto
Event OnActivate(ObjectReference akActionRef)
{Начало блока условия}
If  akActionRef == game.getplayer()
               {Если переменная akActionRef равна переменной game.getplayer(), то…}
akActionRef .additem(MyItem, 1)
               {…совершаем действие – добавляем предмет в инвентарь}
Endif
                  {Конец блока условия}
Endevent

Очень важно, чтобы вы понимали, как работает этот блок проверки условия. Подобные блоки можно вкладывать друг в друга. Например:

If проверка условия №1
   Выполняем действие 1
    If  проверка условия №2
        Выполняем действие 2
             If проверка условия №3
                Выполняем действие 3
            Endif
        Endif
    Endif

Если мы все же хотим выполнить иное действие в случае, если объект был активирован не игроком, можно добавить elseif  (иначе, если…)

If akActionRef == game.getplayer() ;Если активировал игрок, то…
akActionRef .additem(MyItem, 1) ;добавляем предмет
elseif  akActionRef ; иначе
(akActionRef as actor).kill() ;убиваем объект
Endif

Здесь использована функция kill(), мгновенно убивающая актера, вызвавшего её. Но в данном случае akActionRef имеет тип ObjectReference и мы должны привести его к типу Actor, используя “as”.
Еще одним видом проверки является цикл While-do. Он вставляется также в виде блока. Пока выполняется условие, следующее за ключевым словом While, мы  циклически совершаем действие, указанное внутри блока. Обычно условием окончания является достижение переменной-счетчиком какого-либо значения.
Отличие от условия if-then-else:

While пока счетчик
    Совершаем действие 1
    Совершаем действие над счетчиком
Endwhile    Возвращаемся к началу цикла

Например:

While i<=10 ;пока i меньше или равняется 10, выполняем действие
    akActionRef .additem(MyItem, 1)
    i+=1 ;прибавляем к счетчику 1
Endwhile   ;возвращаемся к началу

По окончании действия цикла у актера в инвентаре будет 11 новых предметов.

7.    Функции некоторых объектов

Разработчиками игры созданы очень полезные объекты типа Math, Debug, Game и Utility, которые не являются «расширением» объекта Form. Чтобы использовать функции, доступные в них, можно вызывать их, как мы сделали это для game.getplayer(), либо подключив (или «импортировав») эти объекты как «модули», объявив в теле скрипта с помощью слова Import. Например:

Import Utility
Import Game

В этом случае нам не придется писать каждый раз при обращении к функции game.GetPlayer(), слово game. Просто пишем  - GetPlayer(). Далее привожу некоторые функции этих объектов.

7.1 Функции объекта Math – математические функции:

float Function abs(float afValue) 
Вычисляет абсолютное значение переменной.

float Function acos(float afValue) 
Вычисляет арккосинус.

float Function asin(float afValue) 
Вычисляет арксинус.

float Function atan(float afValue) 
Вычисляет арктангенс.

int Function Ceiling(float afValue) 
Округляет в большую сторону.

float Function cos(float afValue) 
Вычисляет косинус (в градусах)

float Function DegreesToRadians(float afDegrees) 
Конвертирует аргумент в радианы.

int Function Floor(float afValue) 
Округляет в меньшую сторону.

float Function pow(float x, float y) 
Возводит x в степень у.

float Function RadiansToDegrees(float afRadians) 
Переводит аргумент из радиан в десятичную систему.

float Function sin(float afValue) 
Вычисляет синус.

float Function sqrt(float afValue) 
Вычисляет квадратный корень.

float Function tan(float afValue) 
Вычисляет тангенс (в градусах)

Как вы видите, перед функциями стоит параметр int или float. Это означает, что функции возвращают определенную переменную, после того как выполнили над исходными переменными необходимые операции. Для их использования у нас должна быть дополнительная переменная. Например:

MyFloat = Pow(MyNum01, MyNum02)

В результате MyFloat получит число из переменной MyNum01, возведенное в степень MyNum02.

7.2 Основные функции объекта Game - контролирующие игровой процесс:

Function DisablePlayerControls(bool abMovement, bool abFighting, bool abCamSwitch, bool abLooking, bool abSneaking, bool abMenu, bool abActivate, bool abJournalTabs, int aiDisablePOVType)

Отключает управление игры. В качестве дополнительных параметров указываются невозможность двигаться, сражаться, использовать меню и т.п.

Function EnablePlayerControls(bool abMovement, bool abFighting, bool abCamSwitch, bool abLooking, bool abSneaking, bool abMenu, bool abActivate, bool abJournalTabs, int aiDisablePOVType)

Обратная предыдущей.

Function EnableFastTravel(bool abEnable) 
Отключает или включает быстрые перемещения в игре.

Function FastTravel(ObjectReference akDestination) 
Принудительный фаст-тревел. Аргумент – место назначения.

Actor Function FindClosestActor(float afX, float afY, float afZ, float afRadius) 
Находит ближайшего актера, относительно точки, в определенном радиусе.

Actor Function FindClosestActorFromRef(ObjectReference arCenter, float afRadius) 
Находит ближайшего актера относительно другого актера.

Form Function GetForm(int aiFormID) 
Возвращает форму по ее ID.

float Function GetRealHoursPassed() 
Возвращает кол-во часов реального времени, прошедших с момента начала игры.

Function QuitToMainMenu() 
Принудительно выходит в главное меню.

Function PlayBink(string asFileName, bool abInterruptible, bool abMuteAudio, bool abMuteMusic, bool abLetterbox) 
Проигрывает bink видео.

Function IncrementSkill(string asSkillName) 
Увеличивает скилл (только для игрока).

7.3 Функции Utility

float Function GetCurrentGameTime() 
Возвращает прошедшее игровое время в днях.

Float Function RandomFloat(float afMin, float afMax) 
Генерирует случайное значение float от минимального числа до максимального

int Function RandomInt(int aiMin, int aiMax) 
Генерирует случайное значение int.

Function WaitGameTime(float afHours) 
Останавливает выполнение скрипта на определенное игровое время в часах.

7.4 Функции Debug

Function CenterOnCell(string asCellName) 
Телепортирует игрока в определенную локацию.

Function MessageBox(string asMessageBoxText) 
Показывает сообщение типа (текст + ок)

Function QuitGame() 
Выходит из игры.

Function SendAnimationEvent(ObjectReference arRef, string asEventName) 
Посылает анимационное событие определенному объекту.

8.  Создание собственной функции. Расширение скрипта

8.1 Функции

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

Наш активатор раз за разом выдает игроку предметы (у меня это аметисты, у вас он может давать нечто иное, в зависимости от настроенной переменной MiscItem). Мы заставим его раздавать не только аметисты, но и другие драгоценные камни. Очевидный способ увеличить его ассортимент – добавить несколько новых переменных MiscObject, каждой переменной назначить свой параметр и выдать все игроку. Но что, если новых переменных будет несколько сотен  штук? Объявлять 200 одинаковых переменных с уникальными идентификационными именами – абсурд. Хорошо было бы иметь одну большую переменную и засунуть туда все переменные поменьше…

Для операций с огромным количеством однотипных переменных в языке Papyrus предусмотрены массивы (arrays). Это «склады» или по-другому матрицы, содержащие однотипные элементы, каждому из которых присвоен собственный уникальный идентификационный номер. Мы можем обрабатывать эти переменные, пользуясь их номерами.

Условно массив выглядит как: Имя-массива = [0],[1],[2],[3],[4],[5],[…],[N]
Номера элементов целочисленные (integer). Максимальное количество элементов 128. Нумерация начинается с нуля.

Многомерные массивы, к сожалению, в языке Papyrus не предусмотрены (будем дожидаться Skyrim Script Extender (SKSE) – расширителя скриптового языка).

Для объявления массива достаточно указать его имя, тип и количество элементов, содержащихся в нем. Например:

Int[] MyIntegerArray

Здесь int[] объявление массива типа integer, MyIntegerArray- его имя.
В данном случае массив содержит 0 элементов – он пуст.

Float[] MyFloatArray = new float[25]

Здесь мы объявили массив MyFloatArray[] типа Float, содержащий 25 элементов. По-умолчанию все элементы этого массива заполняются нулями. Ключевое слово “new” служит для создания массива.
Мы можем, например, объявить массив:

MiscObject[] MyObjects = new MiscObject[10]

содержащий в себе 10 элементов типа MIscObject, заполненный По-умолчанию значениями None (null).
Мы можем обращаться к каждому элементу массива, просто вызывая его по идентификационному номеру.
Например:

ObjectReference[] Weapon01 = new objectReference[5]
Weapon01[0] = objectRef01
Weapon01[1] = objectRef02
Weapon01[4] = Weapon01[0]

Здесь мы объявили массив Weapon01[], определили, что в нем 5 элементов и затем присвоили первому элементу значение objectRef01, второму элементу значение objectRef02, а пятому элементу присвоили значения первого.

Для обработки массивов часто используются циклы. Например:

Int i
MiscObject[] MyObjects = new MiscObject[10]
While i<10
MyObjects[i] = RandomObject
i+=1
Endwhile

Здесь мы объявляем счетчик i и массив MyObjects[], а затем в цикле каждому элементу массива присваиваем значение RandomObject. Заметьте, что мы обращаемся к элементам массива через счетчик: MyObjects[i], который увеличивает свое значение с каждым циклом.

Мы можем объявить массив как свойство:

ObjectReference[] property MyArray auto

8.2 FormList

Даже используя некоторые ухищрения, вроде циклов, заполнять массив объектов не всегда удобно. В редакторе Creation Kit для этих целей предусмотрены объекты FormList. Это списки, которые могут содержать другие Form внутри себя. Их можно создать непосредственно в окне редактора, задав определенный ID и заполнив объектами. Все FORmlist вы найдете в Object window -> Miscellaneous -> FormList.

Работа с ними аналогична работе с массивами. Создадим новый FormList.
Назовем его aMyFormList. Заполните этот список своими MiscObject, для чего просто перетащите интересующие в окно созданного FormList.



К этому моменту наш скрипт должен выглядеть примерно так:

Scriptname MyFirstScript extends ObjectReference
MiscObject property MyItem auto
Event OnActivate(ObjectReference akActionRef)
                 If  akActionRef == game.getplayer() 
     akActionRef.additem(MyItem, 1)
                elseif  akActionRef
    (akActionRef as actor).kill()
   Endif
endevent

Объявим новую переменную типа FormList:

FormList property MyFormList auto

Значение этой переменной мы должны выбрать в свойствах (property) активатора. Выбираем наш только что созданный FormList.

Для обработки списка применим цикл:

Int i
While i<4
akActionRef.additem(MyFormList.GetAt(i), 1)
i+=1
endwhile

В моем списке четыре объекта. По-умолчанию счетчик равен нулю, но с каждым циклом значение его увеличивается на 1, пока не достигнет четырех. Мы вызываем функцию addItem(), указывая в качестве параметра элемент созданного нами списка, который извлекаем функцией GatAt(), задавая в качестве параметра номер элемента. Таким образом, цикл четырежды добавляет в инвентарь драгоценный камень и каждый раз разный. Но что делать, если мы не знаем длину списка?

Для работы с FormList кроме функции GetAT(), существуют функции:

Function AddForm(Form apForm) 
Добавляет Form в FormList.

int Function GetSize() 
Возвращает длину списка.

bool Function HasForm(Form akForm) 
Возвращает true, если искомая Form находится в списке.

Function RemoveAddedForm(Form apForm) 
Удаляет добавленную Form из списка.

Function Revert() 
Удаляет все формы, добавленные через скрипт.

Теперь, Если мы перепишем условие как:
While i< (MyFormList .GetSize())

Нам не придется знать изначально, сколько элементов содержится в списке.

А теперь давайте добавим не все элементы списка, а какой-нибудь один, но случайный. В этом нам поможет функция объекта Utility RandomInt(), о которой я уже писал.

akActionRef.additem(MyFormList.GetAt(utility.RandomInt(0, MyFormList.getsize())), 1)

Мы вызываем функцию additem() указывая в качестве параметра объект, полученный функцией GetAt(), аргумент которой получен функцией RandomInt(), которая в свою очередь, возвращает случайное значение в диапазоне от нуля до значения, полученного функцией getsize(). Неплохо, да? Но как-то слишком запутано. Не лучше ли нам разделить этот кусок кода на несколько частей?

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

Возвращаемое - значение Function Имя-функции (аргумент01, аргумент02)

Endfunction

Внутри них вы можете объявлять переменные, который будут недоступны другим функциям  того же скрипта. Нам нужно, чтобы функция добавляла выбранный из списка случайным образом объект в инвентарь выбранному актеру. Значит, нам нужна переменная для хранения ссылки на актера(ObjectReference),переменная для хранения кол-ва передаваемых объектов, и мы должны передать функции какой-нибудь FormList:

Function MyFirstFunction(formList akSourceFormList, int count, ObjectReference akActorRef)
        Endfunction

Здесь akSourceFormList – передаваемый нами formList, а akActorRef – ссылка на актера. Давайте получим случайный номер элемента нашего списка

Function MyFirstFunction(formList akSourceFormList,  int count, ObjectReference akActorRef)
int Number = Utility.RandomInt(0, akSourceFormList.getSize())
Endfunction

Как вы видите, мы присваиваем переменной Number случайную integer величину. Теперь нам нужно получить ссылку на объект из formlist:

Function MyFirstFunction(formList akSourceFormList,  int count, ObjectReference akActorRef)
int Number = Utility.RandomInt(0, akSourceFormList.getSize())
Form ValueREf = akSourceFormList.GetAt(Number)
Endfunction

ValueRef мы присваиваем значение формы, полученной функцией GetAt(Number). В последнем шаге мы добавим выбранному актеру полученный объект.

Function MyFirstFunction(formlist akSourceFormList,  int count, ObjectReference akActorRef)
        int Number = Utility.RandomInt(0, akSourceFormList.getSize())
        Form ValueREf = akSourceFormList.GetAt(Number)
        akActorRef.additem(ValueREf, count)
Endfunction

Теперь мы можем вызвать эту функцию для события onActivate:

MyFirstFunction(MyformList, 1, akActionRef)

Как видите, вместо akActionRef.additem(MyFormList.GetAt(utility.RandomInt(0, MyFormList.getsize())), 1) , мы получили аккуратный инструмент для своей работы, который можем использовать и в дальнейшем. Более того, теперь нам не нужно знать, как именно он работает. В окончательно варианте, скрипт должен выглядеть так:

Scriptname MyFirstScript extends ObjectReference
MiscObject property MyItem auto
Event OnActivate(ObjectReference akActionRef)
    If  akActionRef == game.getplayer()
         MyFirstFunction(MyFormList,  1, ObjectReference akActoionRef)
    elseif  akActionRef
        (akActionRef as actor).kill()
    Endif
endevent
Function MyFirstFunction(formlist akSourceFormList,  int count, ObjectReference akActorRef)
        int Number = Utility.RandomInt(0, akSourceFormList.getSize())
        Form ValueREf = akSourceFormList.GetAt(Number)
        akActorRef.additem(ValueREf, count)
endfunction

На этом руководство по скриптам закончена. Чтобы узнать о скриптах больше, воспользуйтесь ресурсом www.creationkit.com, где можно найти описание всех используемых в редакторе функций и объектов.

Автор статьи: Bartolomeo

28

Комментарии

Добавить комментарий

У меня один вопрос. Есть объект(статик). Нужно сделать так, чтобы он на расстоянии 120 единиц был все время перед игроком, не смещаясь ни на единицу от нужных координат..... Как это мне реализовать через скрипт(заметьте, что уровень смещения от нужных координат - перед игроком на расстоянии 120 единиц, где бы игрок не находился, как бы он ни двигался, должен быть не больше нуля, то есть абсолютное отсутствие смещений)?

как с помощью скрипта регулировать параметр вкладки patrol data? а задача такая: есть 3 маркера - 1, 2, 3.. связаны путём. необходимо, чтобы нпс стоящий на  маркере 3 стоял до тех пор пока 2 нпс не встанет на маркер 1

как с помощью скрипта регулировать параметр вкладки patrol data? а задача такая: есть 3 маркера - 1, 2, 3.. связаны путём. необходимо, чтобы нпс стоящий на  маркере 3 стоял до тех пор пока 2 нпс не встанет на маркер 1

это делается через сцены в квесте

1ex0
модмейкер
07.02.2016 — 20:28

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

да мне бы просто скрипт на изменение аутфита спутника сделать) скрипт простой мне кажется, давот не могу никак сообразить его конструкцию. думал может здесь объяснят.

Кто знает, что там со сносом 5-жек?.... у меня такое ощущение, что будут принудительно расселять в жилье с проблемами... как и обычно..

Кто знает, что там со сносом 5-жек?.... у меня такое ощущение, что будут принудительно расселять в жилье с проблемами... как и обычно..

А где можно скачать Construction Set для Ская? Странно, но не нашел на сайте, подскажите, где? В разделе "Программы" нету..

gkalian
администратор
14.04.2021 — 13:56

BDR338, для SE лаунчер официально качается из Bethesda Launcher, а для LE - из Steam.

В разделе 6.1 опечатка. Вместо MyCount01 = (MyFloat01 int) следует указывать MyCount01 = (MyFloat01 as int)

Авторизуйтесь, чтобы оставить новый комментарий. Или зарегистрируйтесь.