Перейти к содержанию
  • NVSE. Часть 2. Пользовательские Функции (UDF)


    Переводчик: ArtSH
    Автор оригинала: DoctaSax

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


    Обратная связь

    Рекомендуемые комментарии

    Комментариев нет



    Для публикации сообщений создайте учётную запись или авторизуйтесь

    Вы должны быть пользователем, чтобы оставить комментарий

    Создать аккаунт

    Зарегистрируйте новый аккаунт в нашем сообществе. Это очень просто!

    Регистрация нового пользователя

    Войти

    Уже есть аккаунт? Войти в систему.

    Войти

×
×
  • Создать...