Это второй из серии гайдов по нововведениям 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, которые вы хотели бы использовать, вы можете его использовать, не копируя код, не делая зависимости к моду и не беспокоится на счет порядка загрузки. 

Материал подготовлен ArtemSH специально для TGM — Tesall Game Magazine.
Переводчик: ArtSH
Автор: DoctaSax
Источник: Перейти
1

Комментарии

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