Это второй из серии гайдов по нововведениям NVSE4, освежившими моддинг сцену Нью Вегаса: UDFы, строковые переменные и массивы. Моддеры Обливиона, незнакомые с ОБСЕ 16+ тоже получат пользу от этих статей.
Данный гайд посвящен теме Пользовательских Функций, User Defined Functions, или же UDF.
Разве вам нравится видеть, как ваши скрипты забиваются кусками кода, которые будут запускаться только изредка, при выполнении должных условий? Видеть, как большая часть вашего скрипта размазана по всему окну редактора с 5-ю уровнями отступа? Или как постоянно повторяются куски кода для каждого случая, пока скрипт просто не превратится в нечто невообразимо длинное? Кошмар!
Разве не хочется хотя бы иногда отдавать часть этого на аутсорс — но тогда придется наплодить кучу скриптов заклинаний или квестовых скриптов для одноразовых задач, что только усугубляет ситуацию, да и заклинания работают только в Gamemode, а скрипты квестовых стадий не могут обрабатывать сложный код или циклы. Не говоря уже о том, что вам приходится дублировать все переменные через квествые скрипты, копируя всю эту чепуху дважды и снова потратив на это сотни строчек кода!
Ну уж нет! Не теперь!
NVSE4+ дает игроку возможность создавать свои собственные функции. В форме скрипта, который можно вызвать, когда это необходимо:
call SomeFunction
Кого вызываем (call)? Какую-то функцию! :)
Пример: если в моде есть настройки, как в меню МСМ, значения настроек по умолчанию выставлятся для всех переменных при запуске мода. И если в них используется buildref, то вызывать эту команду buildref надо каждый раз, когда начинается новая игровая сессия, на случай, если порядок загрузки изменится. Так что люди обычно привязывают это дело к квестовому скрипту, который работает каждый фрейм, в зависимости от переменных iInit или bDoonce.
Begin GameMode
if iInit == 0 ; код работает только в момент инициализации мода в игре
set Var1 to someValue
set Var2 to someValue
set Var3 to someValue
etc etc
set iInit to 1
elseif iInit && fVersion < somefloat ; этот код включается в работу только при апдейте мода
if Var1 < someValue
; do stuff
if someActor.GetAV someActorValue > someValue
; do stuff
endif
else
; do stuff
if somecondition || (somecondition && somecondition)
; do stuff
endif
endif
set fVersion to someFloat
elseif iInit && (GetGameLoaded || GetGameRestarted) ; код работает только при загрузке\рестарте игры
if IsModLoaded "SomeMod"
set iModIndex to GetModIndex "SomeMod"
set rRef to BuildRef iModIndex someDecimalNumber
endif
if IsModLoaded "SomeOtherMod"
; etc.
endif
endif
; наконец, кусок кода, который работает всё время
Не знаю как вы, но я не могу терпеть мысль, что код лежит мертвым грузом, когда инициализируется в игре, но в данный момент не нужен и просто лежит без дела. Конечно, как я уже сказал, часть его можно отрезать олдскульными методами, но как на счет просто отправить эти куски в UDF?
1. Основы UDF структуры и вызова
Создайте обьектный скрипт для будущей UDF. Не прикрепляйте его ни к чему. UDF вызываются напрямую, их нельзя прилепить к вещи или НИПу. Это свободные скрипты в обьектном меню ГЕККа, и если по ним кликнуть и выбрать “use info”, вылезет только тот скрипт, который их, собственно, и вызывает.
UDF могут иметь только один скриптовый блок: Function Block.
scn MyFirstInitFunction ; имя скрипта и будет вызываться (call) вами в вашем вызывающем скрипте (calling script)
; variables
; все переменные должны быть обьявлены до начала блока (это обязательно в UDF, а в остальных скриптах было бы тупизмом так не делать)
Begin Function {} ; не забудьте фигурные скобки
set Var1 to someValue
set Var2 to someValue
set Var3 to someValue
set Var4 to someValue
; etc
End
scn MyUpdateInitFunction
; variables
Begin Function {}
if Var1 < someValue
; do stuff
if someActor.GetAV someActorValue > someValue
; do stuff
endif
else
; do stuff
if somecondition || (somecondition && somecondition)
; do stuff
endif
endif
let MyQuestID.fVersion := someFloat
End
scn MyOnEveryLoadInitFunction
; variables
Begin Function {}
if IsModLoaded "SomeMod"
set iModIndex to GetModIndex "SomeMod"
set rRef to BuildRef iModIndex someDecimalNumber
endif
if IsModLoaded "SomeOtherMod"
; etc.
endif
End
И тогда главному скрипту всего лишь надо вызвать UDF с помощью init.
scn MyMainQstScript
int iInit
Begin GameMode
if iInit == 0
call MyFirstInitFunction
elseif iInit && fVersion < someFloat
call MyUpdateInitFunction
elseif iInit && (GetGameLoaded || GetGameRestarted)
call MyOnEveryLoadInitFunction
endif
; your actual main script
End
Мило и уютно, а? Когда этот квестовый скрипт запускает и встречает UDF, который он должен вызвать, он перестает делать свою работу, затем вызывает UDF и только потом возвращается к той же точке, где его вызывал – и всё это в одном и том же фрейме. И если условие не приложимо, он просто пропускает эту строку.
Но это только начало! Сейчас мы используем эти UDF как хранилища кода чтобы у нас были простые и более читабельные скрипты. А теперь давайте-ка использовать их как настоящие функции. Их же не по приколу называют Заданные Пользователем Функции (User-Defined Functions).
Оригинальная игра имеет довольно много типов функций. Некоторые могут быть вызваны на референсе (функции референциальные, такие как moveto или enable), другие не могут (IsHardCore, IsPC1stPerson). Некоторые возвращают float (математические функции или GetEquipped) или референс (GetActionRef), другие ничего не возвращают (enable, disable). Некоторые включают в себя или прямо требуют параметров (GetAV), другие нет (GetCurrentAIPackage). UDF всё это могут…или не могут, зависит от того, чего вам нужно.
2. Вызов UDF на референсе и Гнездование UDF’ов
В примерах выше я не вызывал UDF на референсе, поскольку в этом не было смысла. То, что происходит в этом момент аналогично вызову результирующего скрипта стадии квеста (quest stage result script), в общем аналогично одно-фреймовому что-то делающему скрипт. Но если вы хотите чтобы ваш UDF что-то сделал с референсом или вернул информацию о нем, то вы вызываете эту функцию прямо как с любыми другими обьектными функциями прямо на референсе.
BuddyRef.call SomeFunction
scn SomeFunction
Begin Function {}
CIOS SomeSpell ; это заклинание будет кастоваться на BuddyRef
End
Как вы видите, прямо сейчас UDF работает как обьектный скрипт в рамках одного фрейма (one-frame-only), привязанный к Buddy Ref, или как скрипт заклинания (spell script), кастующийся на Buddy Ref. Подразумеваемый референс (implied reference) это тот референс, которым вызывается UDF.
(Если хотите отобразить в дебаге какой референс вызвал функцию, вы найдете этот референс с помощью команды GetSelf). И если UDF включает в себя вызов другого UDF, то их взаимосвязь будет такой:
BuddyRef.call someFunction1
scn SomeFunction1
Begin Function {}
; всё остальное
call SomeFunction2
End
scn SomeFunction2
Begin Function {}
CIOS SomeSpell ; this spell is still cast on BuddyRef
End
Независимо от того, вызываете ли вы UDF на референсе или нет, вы можете вложить до 30 штук UDF подобным образом, хотя я считаю, что это немного перебор, и что в таких ситуациях надо пересмотреть всю структуру вашего мода. Также UDF может вызывать сама себя, если сперва линию само-вызова закомментировать и скомпиллировать скрипт.
3. Возврат Значений из UDF
Если нужно UDF может возвращать значение в виде числа, формы, строки или массива. Для этого надо использовать команду SetFunctionValue:
SetFunctionValue fSomeFloat
SetFunctionValue 3
SetFunctionValue rSomeRefVar
SetFunctionValue playerref
SetFunctionValue Scotch
SetFunctionValue sv_SomeStringVar
SetFunctionValue "An actual string"
SetFunctionValue ar_SomeArrayVar
И, конечно же, надо приготовить вызывающий скрипт чтобы адекватно возвратить значение:
let fSomeFloat := call MyUDF
let iSomeInt := call MyUDF
let fSomeFloat := 1 + (call SomeUDF) * 3 ; очевидно, что это будет работать только если UDF возвращает число
let rSomeRefVar := call MyUDF
let sv_SomeStringVar := call MyUDF
let ar_SomeArrayVar := call MyUDF
В случае нашей UDF я сделаю то же самое вот так:
scn MyMainQstScript
...
elseif iInit && fVersion < somefloat
let fVersion := call MyUpdateInitFunction
elseif ...
scn MyUpdateInitFunction
; variables
Begin Function {}
if Var1 < someValue
; do stuff
if someActor.GetAV someActorValue > someValue
; do stuff
endif
else
; do stuff
if somecondition || (somecondition && somecondition)
; do stuff
endif
endif
SetFunctionValue someFloat
End
Вот то же самое с тем же эффектом. Эти штуки лучше прописывать в конце UDF, хотя, по большому счету, это зависит от структуры кода.
scn MyUDF
Begin Function {}
if somecondition
SetFunctionValue someFloat
return
elseif someothercondition
SetFunctionValue someOtherFloat
endif
End
Видите команду return? Как и в других скриптах, она оканчивает работу скрипта, и поскольку UDF срабатывает только один раз и в одном фрейме, мы вернемся напрямую к вызывающему скрипту. Очевидно, что можно проще и быстрее поставить проверки условий в начало UDF, чтобы return стоял прежде обработки более сложной части кода, если его не нужно всегда вызывать. В общем, всё как обычно.
4. Проброс Параметров в UDF
Возможно вы недоумеваете, что это за фигурные скобки (accolades) после команды Begin Function? Ну, в оригинале всё жестко фиксировано в плане того, какие функции могут принимать параметры, а какие нет. А вот NVSE не знает, будете ли вы вообще их включать в свою функцию, или сколько их будет, или что это за параметры, в каком порядке и тому подобное, ока вы сами не укажете это. Именно для этого указания и нужны фигурные скобки.
Вы указываете параметры, обьявляя переменные и включая их в эти фигурные скобки:
scn MyUDF
float fFloat1
int iInt
ref rForm1
ref rForm2
string_var sv_somestring1
string_var sv_somestring2
array_var ar_somearray
; здесь ваши переменные для UDF
Begin Function {fFloat iInt rForm1 rForm2 sv_somestring1 sv_somestring2 ar_somearray}
; скрипт
End
scn MyCallingScript
Begin SomeBlock
call MyUDF fSomeFloat 4 someRefVar playerref sv_somestringvar "Я - строка" ar_somearray
End
Как вы видите, можно использовать конкретные значения или переменные для параметров, когда вызываете UDF, если эти самые UDF могут их хранить (can "catch" them), то есть, к примеру, переменная типа float может хранить число или другую переменную типа float, а переменная типа string, и саму строку и другую такую же переменную типа string. То же самое, но в обратном смысле, работает и с SetFunctionValue.
Как только эти переменные обьявлены параметрами UDF, то есть включены в фигурные скобки, необходимо эти параметры специфицировать в том же количестве, что и в вызывающей функции, и в том же порядке:
call MyUDF fSomeFloat rSomeRef rSomeRef playerref sv_somestringvar "Я - строка" ar_somearray
call MYUDF fSomeFloat 4 rSomeRef playerref sv_somestringvar "Я - строка "
обе эти команды работать не будут: первая, потому что второй параметр должен быть int, а команда вызывает ref, а вторая, потому что параметр массива отсутствует.
Пример 1. Простой пример из документации OBSE:
ScriptName Multiply
float arg1
float arg2
; I like to leave a blank line between parameter vars and local vars, to keep things clear
float localVar ; a local variable
Begin Function {arg1, arg2} ; function body, with parameter list in {braces}
Let localVar := arg1 * arg2
SetFunctionValue localVar ; this is the value that will be returned
End
И вызывающий скрипт будет такой:
float someVar
Let someVar := Call Multiply 10 5
Пример 2. Некоторые оригинальные функции хранят формлисты как параметры, но остальные так не могут. IsSpellTarget – одна из таких уникумов.
scn MyUDF
ref rList
int iCount
ref rSpell
Begin Function {rList}
let iCount := ListGetCount rList
while (iCount -= 1) >= 0
let rSpell := ListGetNthForm rList iCount
if IsSpellTarget rSpell
SetFunctionValue 1
break
endif
loop
End
Тогда вызывающий скрипт будет таким:
if rActor.call MyUDF BoozeList
; актор пьян, омг, сделайте что-нибудь!
endif
if rActor.call MyUDF DrugsList
; актор угашен, омг, сделайте что-нибудь!
endif
Заметьте: в таком случае в наших интересах опустить самые используемые напитки\наркотики в самый низ нашего формлиста, верно?
Пример 3. Предположим, вы устали писать конкретные строки для пользовательского танца с неписем в каждом результирующем скрипте диалога в своем моде:
rActor1.NX_SetEVFl "Dance:Start::CallVer" 1
rActor1.NX_SetEVFo "Dance:Start::ActorA" rActor2
rActor1.NX_SetEVFo "Dance:Start::ActorB" rActor1
rActor1.NX_SetEVFl "Dance:Start::IsTango" 1
rActor1.NX_SetEVFl "Dance:Start::Anim" 201
rActor1.CIOS Dancebegin
набирай, набирай, набирай...
А можно просто вызвать этот UDF:
scn DanceIn
ref rActorA
ref rActorB
int IsTango
int IsBallet
int IsHipHop
int iAnim
Begin Function {rActorA rActorB IsTango IsBallet IsHipHop iAnim}
NX_SetEVFl "Dance:Start::CallVer" 1
NX_SetEVFo "Dance:Start::ActorA" rActorA
NX_SetEVFo "Dance:Start::ActorB" rActorB
NX_SetEVFl "Dance:Start::IsTango" IsTango
NX_SetEVFl "Dance:Start::IsBallet" IsBallet
NX_SetEVfl "Dance:Start::isHipHop" isHipHop
NX_SetEVFl "Dance:Start::Anim" iAnim
CIOS DanceBegin
End
BuddyRef.call DanceIn BuddyRef playerref 1 0 0 201
или
BuddyRef.call DanceIn BuddyRef VeronicaRef 0 1 0 605
Да ерунда! Мы можем расширить этот вызывающий скрипт с помощью параметров refSufarce, fSurfaceX/Y/Z/Angle и RefMoveA/Bto.
К UDF можно притянуть до 10 параметров (15 штук в NVSE 4.5 beta 1).
5. Что Происходит с Нашими Переменными в UDF
Когда UDF выполняет свой код и возвращается к вызывающему скрипту, какие переменные не хранились бы в UDF, параметры уничтожаются и больше ни к чему не отсылают (don't refer to anything anymore). Переменные и значения, которые они содержали, никак не затрагиваются. Это касается всех видов параметров.
Локальные переменные тоже уничтожаются\обнуляются, за исключением одной локальной переменной: переменной типа string. Строки, на которые ссылаются локальные строковые переменные UDF, не уничтожаются и продолжат находится в .nvse файле, пока вы сами их не уничтожите, что можно сделать вот так:
sv_destruct mystringvar
6. Другие Приколюхи
Поскольку UDF – независимые скрипты, которые все равно отображаются как обьектные скрипты, на них можно ссылаться с помощью переменной типа ref. И делать почти то же самое, что и с остальныеми переменнами типа ref, ссылающимися на базовые формы без конкретного референса в игре (world model). То есть, UDF нельзя сдвинуть командой moveto, но её можно содержать в формлисте или массиве, возвращать её formID в дебагге с помощью %i, применять команду NX_SetEVFo, и использовать set в зависимости от условий.
if somecondition
let someRefVar := someUDF1
elseif somecondition
let someRefVar := someUDF2
endif
call someRefVar
И коль скоро её можно хранить в переменной типа ref, на неё работает команда buildref!
set someRefVar to BuildRef someModIndex somedecimalint
call SomeRefVar
Это означает, что, если у какого-то другого мода есть классные UDF, которые вы хотели бы использовать, вы можете его использовать, не копируя код, не делая зависимости к моду и не беспокоится на счет порядка загрузки.
Комментарии