WarCraft 3: Оптимизация триггеров и jass кода

» Раздел: Триггеры и объекты

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

Переменные

Типы переменных

Переменные это, если иначе сказать, - ссылки. Типов переменных очень много, даже больше чем стандартных которые находятся в “редакторе переменных” в “редакторе триггеров”. Но основных типов только 6:
  • Boolean (логическая)
    Переменные принимающие значение Да/Нет
  • handle (указатель)
    handle это указатель, указывающий на реальный игровой объект, сам по себе объект находиться в переменной не может, и потому при действиях над переменой, мы совершаем действие над объектом, который находится по указателю из этой переменой. У типа “handle” есть много наследников, которые тоже имеют наследников:
    Например: Тип переменой “unit” (указатель на юнита) является наследником типа переменой “widget”, которая в свою очередь является наследником “handle”.
    Т.е. это означается что каждый “unit” является “handle”, но не каждый “handle” является “unit”.
  • code (функция)
    В этой переменой может содержаться функция, вроде бы все должно быть понятно, но Только минус этого типа в том что он не может являться массивом и быть созданным удаленым или измененым во время игры.
  • string (строка)
    Просто набор символов
  • integer (целое число)
    Целые числа, могут принимать значение от -2147483647 до 2147483647
  • real (действительное число)
    Числа с плавующей запятой
Есть типы переменных (не integer'ы), которые тоже могут принимать целое значение, например “unittype”, хотя это переменная не является integer'ом, она ее заменяет для типов юнитов (для удобства).
integer-ы в варе могут быть в 4-ех системах исчисления:
  • 8-ми значная
    Чтобы указать, что это именно 8-ти значная, перед значением ставят “0”, т.е. “0<значение>
  • 10-ти значная
    Ну это обычные числа
  • 16-ти значная
    Чтобы указать, что это именно 16-ти значная, перед значением ставят “0x”, т.е. “0x<значение>
  • 256-ти значная
    Чтобы указать, что это именно 256-ти значная, значение выделяют одинарными кавычками, т.е. “ <значение>
Когда вы пользовались триггерами, вам было необходимо при математических действиях, чтобы внести в целую переменную реальное число – вам надо было перевести его сначала в целое с помощью определенной триггерной функции в типе “Преобразование”, это все правильно. Т.к. чтобы внести в целое число реальное, надо сначала реальное представить в виде целого. НО:
При внесении в реальную переменную целого числа, его надо было переводить в реальное число, в джазе этого делать необязательно, т.к. по сути это разные типы переменных.. но вар автоматически использует приведение целое к реальному числу, а тратить время, чтобы перевести его в реальное с помощью функции I2R(I) необязательно. А вот при возвращении функцией, какое либо число real/integer приведения не происходит, потому нужно точно указывать тип переменой.

Обнуление

У переменых типа integer, real, boolean есть некая “область видимости”, по выходу из этой области – переменная удаляется. У всех остальных переменных этой “области” нет, а значит, их нужно обнулять вручную
Set <переменная> = null
Если ее не обнулить, то она останется в памяти компьютера навсегда по протяжению игрового процесса, и тем самым в большом количестве будет вызывать тормоза!

Функции

Их виды:
  • native функции
Это функции, которые находятся непосредственно в самом движке вара, и не могут быть изменены или добавлены стандартными методами. Они находятся в common.j
  • BJ функции
Это функции, которые являются дополнительными и могут быть созданы самим джазером, они состоят из native функции и/или других BJ функций. Они находятся в blizzard.j
native функции не вызывают утечек если ими пользоваться правильно, а вот четверть функций BJ вызывают утечку даже при правильном с ними обращении.
Одна из многих проблем этих функций в том, что в них создаются локальные переменные типа “handle” и не обнуляются в конце, и вызывают лишнее загрязнение памяти (иначе - "утечки"), и именно поэтому большинство джаззеров (включая меня) настроены против BJ. Я всегда пользуюсь native функциями (в 80% случаев), а юзаю BJ - только в том случае, если они не вызывают утечек, и содержат много других используемых функций (потому что иначе пришлось бы один и тот же массив функций писать в большинстве кодов, и это приводило бы к “очень большому коду” и “трудно читаемости”).
Пример таких функций, которые вызывают утечки это практически все BJ функции по изменению multiboard-а - они не только громоздкие (из-за того, что сделаны двойными циклами т.к. рассчитаны на указание в их параметры нуля), но еще вызывают утечку из-за не обнуленной переменой - потому их можно сократить…

К примеру, вот так:

Обычная BJ функция по изменению текста в ячейке multiboard-а:
function MultiboardSetItemValueBJ takes multiboard mb, integer col, integer row, string val returns nothing
    local integer curRow = 0
    local integer curCol = 0
    local integer numRows = MultiboardGetRowCount(mb)
    local integer numCols = MultiboardGetColumnCount(mb)
    local multiboarditem mbitem = null    //!Эта переменная, при присвоении ей позже значения не обнуляется в конце кода!
    loop
        set curRow = curRow + 1
        exitwhen curRow > numRows
        if (row == 0 or row == curRow) then
            set curCol = 0
            loop
                set curCol = curCol + 1
                exitwhen curCol > numCols
                if (col == 0 or col == curCol) then
                    set mbitem = MultiboardGetItem(mb, curRow - 1, curCol - 1)
                    call MultiboardSetItemValue(mbitem, val)
                    call MultiboardReleaseItem(mbitem)
                endif
            endloop
        endif
    endloop
endfunction
Это то как это можно сократить - жертвуя относительно немногим:
function MultiboardSetItemValueBJ takes multiboard mb, integer col, integer row, string val returns nothing
    local multiboarditem mbitem = MultiboardGetItem(mb, row - 1, col - 1)
    call MultiboardSetItemValue(mbitem, val)
    call MultiboardReleaseItem(mbitem)
    set mbitem = null
endfunction
И есть еще много BJ функций, которые сами по себе независимо вызывают утечки.
Также все функции требуют время на вызов, чем больше это время тем сильнее будет подтормаживать игра во время их вызова, но правда это время измеряется долями микросекунды, так что в не динамических триггерах это имеет малое значение, ну а вот в динамических...:
Это означает что плохо вызывать функции, которая вызывает другую нужную нам функцию.
Например:
function DestroyEffectBJ takes effect whichEffect returns nothing
    call DestroyEffect(whichEffect)
endfunction
По сути это функция делает то же что и в ней содержащаяся, так какой смысл юзать функцию DestroyEffectBJ? Если можно напрямую воспользоваться DestroyEffect. И это весьма слабый пример, некоторые функции вызывают функции нужные нам только через 2-4 функции:
Например:
Нам нужно создать одного юнита - мы это делаем на триггерах с помощью специальной функции - *CreateNUnitsAtLoc*:
function CreateNUnitsAtLoc takes integer count, integer unitId, player whichPlayer, location loc, real face returns group
    call GroupClear(bj_lastCreatedGroup)
    loop
        set count = count - 1
        exitwhen count < 0
        call CreateUnitAtLocSaveLast(whichPlayer, unitId, loc, face)
        call GroupAddUnit(bj_lastCreatedGroup, bj_lastCreatedUnit)
    endloop
    return bj_lastCreatedGroup
endfunction
Обратите внимание, что там все происходит через цикл, в котором используется сама функция создающая юнита, а именно *CreateUnitAtLocSaveLast*:
function CreateUnitAtLocSaveLast takes player id, integer unitid, location loc, real face returns unit
    if (unitid == 'ugol') then
        set bj_lastCreatedUnit = CreateBlightedGoldmine(id, GetLocationX(loc), GetLocationY(loc), face)
    else
        set bj_lastCreatedUnit = CreateUnitAtLoc(id, unitid, loc, face)
    endif
    return bj_lastCreatedUnit
endfunction
В ней уже используются if-ы, нормальное создание юнитов уже выполняется с помощью нормальной native функции - *CreateUnitAtLoc*:
native CreateUnitAtLoc takes player id, integer unitid, location whichLocation, real face returns unit
И что получается? В триггерах мы пользовались функцией для создания юнита, которая через 2 функции, через цикл, и через if и создавала юнита, а все можно было сделать сразу с помощью одной native функции!
Огромное количество подобных недочетов в периодических триггерах может тормозить игру.
Просмотр переменой занимает намного меньше времени чем вызов функции, как этим можно воспользоваться, а очень просто, если у вас в коде во многих местах вызывается одна и та же функция, то намного лучше было бы с самого начала занести эту функцию в переменную, а потом использовать именно переменную.
Самый простой пример:
function Func takes nothing returns nothing
    if GetUnitState(GetTriggerUnit(), UNIT_STATE_MANA) < 1000 then
        if GetUnitState(GetTriggerUnit(), UNIT_STATE_MANA) > 500 then
            call SetUnitState(GetTriggerUnit(), UNIT_STATE_LIFE, RMaxBJ(0,GetUnitState(GetTriggerUnit(), UNIT_STATE_LIFE) - 500))
        endif
        if GetUnitState(GetTriggerUnit(), UNIT_STATE_MANA) <= 500 then
            call KillUnit (GetTriggerUnit())
            call TriggerSleepAction (5)
            call RemoveUnit (GetTriggerUnit())
        endif
    endif
endfunction
Эта функция делает GetTriggerUnit() мертвым, если жизней меньше 500, или отнимает 500, если у него жизней больше 500, но меньше 1000
Обратите внимание, как много в ней функций, практически половину из них можно убрать, используя переменные. Вот что должно получиться:
function Func takes nothing returns nothing
    local unit u    = GetTriggerUnit()
    local real live = GetUnitState(u, UNIT_STATE_MANA)
    if live < 1000 then
        if live > 500 then
            call SetUnitState(u, UNIT_STATE_LIFE, live - 500)
        endif
        if live <= 500 then
            call KillUnit  (u)
            call TriggerSleepAction (5)
            call RemoveUnit(u)
        endif
    endif
    set u = null
endfunction
Обратите внимание, что мы сократили функции GetTriggerUnit и GetUnitState, и, в общем, количество функций уменьшилось на 8. Да и текст принял вполне читаемый вид (Это тоже имеет довольно важное значение).

Игровые объекты

Мы рассмотрели переменные, и как их обнулять, переменная - это ссылка на определенный игровой объект, но обнуление переменой не значит что и удалится объект по ссылке из переменой, объект нужно удалять отдельно (ДО(!) обнуления переменой) с помощью специальной функции. т.к. иначе после обнуление переменой ссылка на объект потеряется, и его нельзя будет удалить.
К примеру эффекты удаляются с помощью DestroyEffect
Юниты: RemoveUnit
Декорации: RemoveDestructable
Точки: RemoveLocation
Триггеры: DestroyTrigger
Таймеры: DestroyTimer
и т.д.
Правда некоторые объекты не удаляются... т.е. DestroyTrigger не полностью удаляет триггер... перед этим еще нужно удалить все его действия выключить подождать секунду и потом удалить.. и обнулить
Но даже это может не спасти от утечек.. потому между выбором - таймер или периодический триггер - выбирайте таймер.
Например - спел... в нём создается много эффектов, и не удаляются, а это значит, что информация по положению эффекта, по его типу остается в памяти на всегда. Обращаться с локальными переменными между функциями очень трудно, т.к. обязательно нужно следить, чтобы ссылка на этот спецэффект не была потеряна, и чтобы он рано или поздно был удален из игры, тоже касается юнитов (в основном имеется в виду "дами-юниты").
Самая распространенная утечка - это точки, они очень слабо загрязняют память компа, но их так много! что это одна из серьезных утечек. Например, давайте разберем простое периодическое перемещение юнита: на триггерах эта выглядит просто... событие в период 0.04, и в нем движение юнита в какую не было сторону, НО большинство триггерщиков используют в подобных перемещениях полярные координаты - что очень ошибочно, т.к. они вызывают куча утечек, вот к примеру разберем периодический триггер с действием:
Боевая единица - Move unit instantly to ((Position of unit) offset by len towards ang degrees), facing ang degrees
где:
unit - переменная перемещаемого юнита
len - расстояние, на которое будет перемещен юнит
ang - Под каким углом
Этот триггер переведенный в джазз:
function Trig_go_Actions takes nothing returns nothing
    call SetUnitPositionLocFacingBJ( udg_unit, PolarProjectionBJ(GetUnitLoc(udg_unit), udg_len, udg_ang), udg_ang )
endfunction

//===========================================================================
function InitTrig_go takes nothing returns nothing
    set gg_trg_go = CreateTrigger(  )
    call TriggerRegisterTimerEventPeriodic( gg_trg_go, 0.04 )
    call TriggerAddAction( gg_trg_go, function Trig_go_Actions )
endfunction
давайте обращу внимание на недочеты в этой функции:
  • TriggerRegisterTimerEventPeriodic
function TriggerRegisterTimerEventPeriodic takes trigger trig, real timeout returns event
    return TriggerRegisterTimerEvent(trig, timeout, true)
endfunction
Если вы поняли о чем говорилось раньше... то эту функцию можно сократить другой, которую она использует.
  • SetUnitPositionLocFacingBJ
function SetUnitPositionLocFacingBJ takes unit whichUnit, location loc, real facing returns nothing
    call SetUnitPositionLoc(whichUnit, loc)
    call SetUnitFacing(whichUnit, facing)
endfunction
также можно заменить двумя отдельными функциями - отдельно перемещение, отдельно поворот.
  • GetUnitLoc
constant native GetUnitLoc takes unit whichUnit returns location
эта функция создает точку, которая не удаляется.
  • PolarProjectionBJ
function PolarProjectionBJ takes location source, real dist, real angle returns location
    local real x = GetLocationX(source) + dist * Cos(angle * bj_DEGTORAD)
    local real y = GetLocationY(source) + dist * Sin(angle * bj_DEGTORAD)
    return Location(x, y)
endfunction
эту функцию во первых можно раскрыть, во вторых она тоже создает точку которая не удаляется.
это получается 50 не удаленных объектов в секунду. подобный триггер будет слабо как-то влиять на игру... а что если таких триггеров будет 10, и они будут создавать еще больше лишних точек - тогда игра начнет тормозить уже через несколько минут.
Исправим все эти недочеты:
function Trig_go_Actions takes nothing returns nothing
    local real X = GetUnitX(udg_unit)+Cos(udg_ang*0.0174)*udg_len
    local real Y = GetUnitY(udg_unit)+Sin(udg_ang*0.0174)*udg_len
    call SetUnitPosition(udg_unit, X, Y   )
    call SetUnitFacing  (udg_unit, udg_ang)
endfunction

//===========================================================================
function InitTrig_go takes nothing returns nothing
    set gg_trg_go = CreateTrigger()
    call TriggerRegisterTimerEvent( gg_trg_go, 0.04, true               )
    call TriggerAddAction         ( gg_trg_go, function Trig_go_Actions )
endfunction
Обратите внимание на Cos(udg_ang*0.0174) и тот же Sin, почему так?
а просто BJ функция
function CosBJ takes real degrees returns real
    return Cos(degrees * bj_DEGTORAD)
endfunction
использует обычные градусы, и умножает на bj_DEGTORAD, чтобы перевести их в радианы, а сама функция Cos уже и находит косинус радиан этого угла, но т.к. bj_DEGTORAD - константа, то можно сразу вместо нее записать численное значение, это ~ 0.0174.
И смотрите: нет ни одной точки, если есть выбор между юзанием точек или ее координат, я выбираю координаты. Для все native функций работающих на точках, есть аналоги работающие на их координатах, например: SetUnitPositionLoc и SetUnitPosition, CreateUnitAtLoc и CreateUnit, и т.д.
И что получилось? код начал работать в два раза быстрее и без утечек.
Любой объект надо обязательно создавать в переменной, если мы будет создавать ее как оператор функции, например, так:
call SetUnitPositionLoc (udg_unit, GetUnitLoc(udg_unit))
То ссылка на точку GetUnitLoc(udg_unit) затеряется, и мы не сможем указать какой объект надо удалить, потому надо делать все через переменные:
local location loc = GetUnitLoc(udg_unit)                                
call SetUnitPositionLoc (udg_unit, loc)
call RemoveLocation (loc)
set loc = null

Проверка

Как же проверить, много утечек или нет? Очень просто, каждый игровой объект имеет уникальный номер, его можно получить используя эту систему:
function HandleCounter_Update takes nothing returns nothing
   local integer i = 0
   local integer id
   local location array P
   local real result=0
   loop
      exitwhen i >= 50
      set i = i + 1
      set P[i] = Location(0,0)
      set id = GetHandleId(P[i])
      set result = result + (id-0x100000)
   endloop
   set result = result/i-i/2
   loop
      call RemoveLocation(P[i])
      set P[i] = null
      exitwhen i <= 1
      set i = i - 1
   endloop
   call LeaderboardSetItemValue(udg_HandleBoard,0,R2I(result))
endfunction

function HandleCounter_Actions takes nothing returns nothing
   set udg_HandleBoard = CreateLeaderboard()
   call LeaderboardSetLabel(udg_HandleBoard, "Handle Counter")
   call PlayerSetLeaderboard(GetLocalPlayer(),udg_HandleBoard)
   call LeaderboardDisplay(udg_HandleBoard,true)
   call LeaderboardAddItem(udg_HandleBoard,"Handles",0,Player(0))
   call LeaderboardSetSizeByItemCount(udg_HandleBoard,1)
   call HandleCounter_Update()
   call TimerStart(GetExpiredTimer(),0.05,true,function HandleCounter_Update)
endfunction

function InitTrig_HandleCounter takes nothing returns nothing
   call TimerStart(CreateTimer(),0,false,function HandleCounter_Actions)
endfunction
Уникальные номера всех создаваемых объектов типов handle выстраиваются по порядку, если какой-то из объектов удаляется, его уникальный номер освобождается, и его место займет следующий созданный объект.
Тут создаем периодический триггер в 0.05 сек., на экран будут выводиться цифры... начиная с "миллиона с копейками", значение имеет не величина этого номера, а его то насколько он изменяется в процессе игры...
Однако не стоит так на этом засиживаться... избавиться от всех утечек всеравно невозможно... так что не принимайте это слижком всерьез.

Просмотров: 6 001

» Лучшие комментарии


Zahanc #1 - 5 лет назад 2
Узнал много полезного, особенно что касается BJ.
ashez #2 - 5 лет назад 2
Великолепная статья, очень мне помогла.
Daro #3 - 4 года назад 0
буду учить
Это сообщение удалено
Singularity #5 - 1 неделю назад (отредактировано ) 2
Важная тема. Уметь правильно собирать мусор скрипта очень важно для повышения производительности. Однако, вопреки некоторым комментаторам выше, до статуса великолепной этой статье ещё расти и расти. До того, как писать к статье комментарий, прочёл её раз десять, затем применил свои рекомендации в местной песочнице, чтобы убедиться, что все советы, которые будут даны, действительно помогут статье стать лучше.
Заметки на полях:
» Куча текста, открывать на свой страх и риск!
  1. Не обнаружил оглавления. Оглавление - важная часть любой статьи, даже самой маленькой. Согласно статье в Википедии, оглавление ускоряет поиск частей статьи. А если читатель найдёт именно то, что ему нужно (не только найдёт, но и поймёт написанное), он охотно кинет Вам и спасибку, и печеньку, и плюсик в рейтинг. С точки зрения читателя сейчас статья смотрится как один большой "ой-куда-меня-занесло". Отсутствие оглавления создаёт впечатление, что автор статьи ставит своей целью не лаконичное изложение читателю необходимой информации, а собственное самоутверждение. Искренне верю, что это не так и что автор исправится, добавив содержание в начало статьи и упростив читателю навигацию.
  2. Зачем изобретать колесо? Здесь я имею в виду перечисление и описание типов переменных. Для этого на сайте есть определённый цикл статей, где должны быть приведены как основные типы переменных, так и их описания. Заботьтесь о читателе, создавая не "костыли" в виде краткого пересказа общей информации, а грамотную ссылочную структуру на эту самую информацию. Так читателю будет дан максимум необходимой общей информации, а к Вашей статье он направится за разъяснением конкретного случая (в данной статье этот случай - тонкости оптимизации триггеров и JASS-скриптов). Вы будете не конкурировать с другими статьями, а органично дополнять их, ведь хороший автор ставит своей целью не унижение других авторов, а их дополнение либо разъяснение того, что было упущено в их работах, не уменьшая вместе с тем важность их вклада в развитие сегмента. Все авторы обучающих статей фактически делают общее дело (лаконичное донесение необходимой читателю информации), так что здесь не должно быть внутренних скрытых распрей. Прошу прощения, если зря Вас обидел своим мировоззрением.
    Так как Ваша статья находится на стыке GUI и JASS, отличной идеей будет предоставление ссылок как на обучающие статьи по JASS, так и на обучающие статьи по GUI (таким образом, статья будет прочитываться на одном дыхании).
  3. Наличие больших кусков кода в тексте статьи существенно затрудняет её восприятие. Большие фрагменты кода лучше прятать под спойлеры. Если читатель нуждается в просмотре кода, он откроет спойлер. Другим читателям, которые, скажем, уже изучали "начинку" BJ-функций и знают её, это существенно облегчит прочтение статьи, а, значит, и усвоение материала.
  4. Будьте ближе к читателю. Откажитесь от использования профессионального сленга там, где можно без него обойтись ("юзать", "джаззер", "триггерщик"). Читатели будут благодарны за то, что Вашу статью легко читать даже новичку.
  5. Многие разработчики модификаций WarCraft III забывают, что JASS - это не слово, а аббревиатура. В частности из-за того, что во многих статьях JASS не пишут в верхнем регистре.
  6. Стоит подчеркнуть, что тип location в чистом JASS рационально использовать только в одном случае - при получении высоты ландшафта в определённой точке (функция GetLocationZ). Для всего остального есть получение координат X и Y.
  7. Хорошо бы добавить статье раздел TL;DR (краткое содержание, аббревиатура расшифровывается как Too Long; Didn't Read - слишком длинно; не читал). Это в своём идеальном варианте таблица с колонками: Тип переменной - Способ очистки в JASS - Способ очистки в GUI. Это позволит ничего не упустить.
Не ставлю минуса - изложение хорошее, но и плюса - много мелочей для доработки.
Надеюсь, что рекомендации действительно оказались полезны.
Всегда ваш,
Singularity, 16.06.2017