Правило 1: Удаляйте ненужные вызовы функций. Вступление.
При написании кода вы должны сделать как можно меньше вызовов функций. Номер, который считает вызовы, это ОБЩЕЕ количество, это не количество вызванных функций в вашей функции. Если вызвать функцию, содержащую в себе вызовы 3-х функций, вы вызовете 4 функции. Если вызвать эти 3 функции напрямую, тогда вы избавитесь от вызова лишней функции.
Если вы вызовете GetTriggerUnit() в своей функции трижды, лучше использовать переменную, в которой будет храниться значение GetTriggerUnit(). В этом случае вы избавитесь от вызова 2-х лишних функций.
Конечно же, это баланс между созданием коротких кодов и небольшим вызовом функций. Однако если использованная вами функция вызывает 40 функций, вызов их напрямую сделал бы код намного длиннее, совсем не стоит этого делать ради того, чтобы вызвать на одну функцию меньше.
Используйте Native-функции.
Большинство BJ-функций (Blizzard.j) - это просто дополнительные функции, которые вызывают Native-функции (common.j). Если вы вызовете BJ-функцию такого типа, то 100% получите больше вызовов функций, чем если бы вы вызывали Native-функцию напрямую. Поэтому перед тем, как использовать BJ, подумайте, как можно заменить её на Native. Это основное правило JASS.
Изучайте функции.
Самый простой способ посмотреть начинку функции - найти программу, которая содержит списки функций. Для этих целей можно использовать
JASS NewGen Pack или
JassCraft. Если вы не хотите использовать программу, просто поищите нужную функцию на сайте
wc3jass.com.
Самый простой способ сказать, что вызов функции не требуется и нужно вызвать функцию, которая находится внутри - посмотреть на её имя. Если оно заканчивается на BJ, вызов такой функции в большинстве случаев является лишним. Однако есть и исключения, некоторые BJ-функции действительно что-то делают и пригождаются. Некоторые функции из Blizzard.j могут не содержать BJ в конце своего имени.
Пример.
После того, как вы немного ознакомились с теорией, давайте разберём простой триггер:
Код
Unit Dies
События Боевая единица - A unit Умирает
Условия
(Triggering unit) is Герой) равно Да
Действия
Игра - Display to (All players) for 30.00 seconds the text: ((Name of (Owner of (Triggering unit))) + ( was killed by + (Name of (Owner of (Killing unit)))))
Игрок - Add ((Level of (Triggering unit)) x 50) to (Owner of (Killing unit)) Золото(текущ.)
Wait ((Real((Level of (Triggering unit)))) x 10.00) seconds
If (All Conditions are True) then do (Then Actions) else do (Else Actions)
If - Conditions
((Triggering unit) belongs to an ally of Игрок 1(Красный)) равно Да
Then - Actions
Герой - Instantly revive (Triggering unit) at (Center of Team 1 Base <gen>), Показать revival graphics
Камера - Pan camera for (Owner of (Triggering unit)) to (Center of Team 1 Base <gen>) over 2.00 seconds
Else - Actions
Герой - Instantly revive (Triggering unit) at (Center of Team 2 Base <gen>), Показать revival graphics
Камера - Pan camera for (Owner of (Triggering unit)) to (Center of Team 2 Base <gen>) over 2.00 seconds
Это отличный триггер воскрешения героев. Будучи пользователем JASS, вы скажете, что здесь есть утечки. Сейчас мы сконвертируем этот триггер в код и устраним утечки. Получится что-то вроде этого:
Этот скрипт уже безутечен, однако ещё далеко не совершенен. Он прекрасно работает и не лагает. Давайте теперь его немного оптимизируем.
Начнём с конца триггера. InitTrig_Unit_Dies довольно неплох, однако в нём есть небольшая заминка - TriggerRegisterAnyUnitEventBJ. В некоторых случаях следует провести чёткую черту между понятиями "Длина функции" и "Использование Native". И я считаю, что раскрывать TriggerRegisterAnyUnitEventBJ (7 строк) не следует только из-за того, что мы хотим только Native-функции.
Но вы можете выбирать сами, что делать в подобных ситуациях. Автор решил раскрыть TriggerRegisterAnyUnitEventBJ. Ниже его код:
Примечание автора. Я сразу задал значение переменной index. Кто-то может вначале объявить её, а потом задать. Лучше этого не делать и задать значение сразу же после объявления.
Примечание переводчика. Видно, автор не пожелал избавиться от gg_trg посредством local trigger.
Итак, мы закончили с первой функцией. Теперь перейдём к функции Trig_Unit_Dies_Actions. Так как функция Trig_Unit_Dies_Func004C принадлежит к Trig_Unit_Dies_Actions, можно взять их вместе.
Здесь побольше работы. Прежде всего нужно заметить, что мы очень часто используем GetTriggerUnit() и GetKillingUnitBJ(). Это не очень хорошо. Объявите несколько локальных переменных в начале функции и используйте их.
Помните: Если это возможно, задавайте значение локальной переменной вместе с объявлением.
После того, как мы это сделаем, наша функция будет выглядеть так:
Примечание 1. Так как "if" - это другая функция, нельзя использовать там наши локальные переменные. Это плохо и вообще не имеет здесь смысла держать отдельную функцию для условия, поэтому впишем его в основную функцию.
Примечание 2. GetKillingUnitBJ() - это функция, возвращающая GetKillingUnit(), поэтому мы уберём BJ и напишем GetKillingUnit().
if более сложен, чем BJ, поэтому я объясню поподробнее.
Вы понимаете, что if, elseif, exitwhen и.т.п. - просто логические значения. Логические значения дают true или false.
Можно написать "true == true", как и просто "true". Или же можно написать "FunctionThatReturnsBoolean() == true", хотя лучше будет написать "FunctionThatReturnsBoolean()". Если FunctionThatReturnsBoolean() правдива, тогда выводимая логическая будет true. Написать "FunctionThatReturnsBoolean()" будет намного проще, чем "FunctionThatReturnsBoolean() == true".
Так как функция Trig_Unit_Dies_Func004C возвратится в случае, если Player(0) (Игрок 1 (Красный) в GUI) является союзником GetTriggerUnit(), можно заменить всё на "IsUnitAlly(victim, Player(0))".
Вместе с предыдущими изменениями мы получим это:
Примечание. Функция Trig_Unit_Dies_Func004C стала бесполезна, поэтому мы удалили её.
Код выглядит всё лучше и лучше.
Теперь мы видим ещё одну вещь - различие между if и else только лишь в значении переменной l. Остальное можно вынуть из этого блока, эта операция сделает код короче. После того, как мы немного почистили код, мы можем сделать ещё 4 вещи:
- Создать переменную типа "игрок" со значением GetOwningPlayer(victim) и использовать её.
- Создать переменную типа "игрок" со значением GetOwningPlayer(killer) и использовать её.
- Создать переменную типа "целочисленная" со значением GetUnitLevel(victim) и использовать её.
- Удалить ненужную конвертацию R2I. Целочисленные могут быть реальными без каких бы то ни было операций, но для обратной операции нужна конвертация R2I, иначе компилятор не скомпилирует код нормально.
После этого мы немного уменьшили итоговое кол-во вызванных функций. Это то, чего мы и хотели.
Вот код, который получился после всех операций, которые я указал:
Вам нужно запомнить 2 вещи:
- Я удалил некоторые скобки, используйте их только для случая, когда вы хотите посчитать одно перед другим. Например 3*(2+4), но не (3*2)+4, так как 3*2 по умолчанию считает перед тем, как прибавить 4. Но будьте внимательны: Не удалите случайно скобку, нужную для вызова функции.
- Эта функция длиннее предыдущей, но беспокоиться не стоит. Это не длина, которая важна, это число вызовов функций.
На этой стадии осталось сделать 2 вещи:
- DisplayTimedTextToForce не очень хороша. Мы же хотим вывести текст всем игрокам, поэтому нам не нужно проверять, находятся ли они в отряде.
Подсказка: Внимательно посмотрите начинку этой функции:
Эта функция проверяет, есть ли в заданном отряде GetLocalPlayer(), затем выводит текст для этого игрока. Так как мы хотим, чтобы текст отображался для всех игроков, мы можем использовать "call DisplayTimedTextToPlayer(GetLocalPlayer(), .0, .0, duration, message)" напрямую.
- AdjustPlayerStateBJ - это BJ-функция, которая имеет значимость. Она также обновляет кол-во "Всего добытого" ресурса. Но только лишь если изменение - позитивное число. Зная то, что значение здесь позитивное, можно здесь многое изменить.
После всего этого мы получим такой код:
Пока что мы оставим эту функцию в покое, однако мы к ней ещё вернёмся.
Последняя функция лёгкая, но здесь есть одна хитрость: здесь есть баг со стороны Blizzard, и поступать, как описано ниже, нужно только в этом специфическом случае.
Мы можем её упростить.
Если НЕ IsUnitType(GetDyingUnit(), UNIT_TYPE_HERO) == true, тогда вернётся false, в противном случае вернётся true. Подумайте, оно же просто вернёт значение IsUnitType(GetDyingUnit(), UNIT_TYPE_HERO) верно? Так почему бы не написать это вместо всего остального?
Итак, по поводу ошибки. Это условие, т.к. это функция, использованная в триггере и возвращающая логическое значение. Здесь не использована отдельная функция, какая наблюдалась в предыдущем условии. В этом условии мы используем функцию IsUnitType(), вот тут баг и начинается. IsUnitType() ДОЛЖЕН с чем-либо сравниваться, например IsUnitType() == true, иначе мы получим баги. Сейчас UNIT_TYPE_HERO и RANGED_ATTACKER - это единственные значение, которые не дают здесь багов (странно, не правда ли?). Однако для хорошей практики я всегда предотвращаю баг при использовании этой функции.
Перепишем эту функцию.
Итак, мы закончили первичную оптимизацию. Вот код, который у нас получился:
Когда вы будете более опытны, вы сможете перепрыгивать через всё и оптимизировать код напрямую. Когда вы будете на этой стадии, то вы сможете писать код без базы на триггерах. Использовать GUI вы будете только в целях генерации новых "триггеров", блоков, которые вы сможете использовать в своём коде.
Это ещё одна причина использовать программу для редакции JASS-кода. Тогда у вас будет лёгкий доступ ко всем функциям и их начинке.
Правило 2: Используйте х,y вместо Location. Координаты Х и Y имеют одно большое преимущество перед локациями - они не нуждаются в обнулении. Единственные случаи, когда действительно необходимо использовать локации - это если вы используете GetSpellTargetLoc и GetLocationZ, для них нет эквивалентов Х и Y.
Примечание переводчика: В 1.24 появились новые функции GetSpellTargetX() и GetSpellTargetY(). GetSpellTargetLoc уже может быть заменена, однако GetLocationZ останется проблемой.
Если у вас уже есть локация, тогда преобразуйте её. Если у вас её нет, тогда сразу используйте координаты Х и Y.
Большинство функций используют реальные, но только после того, как вытащить их оттуда. К примеру, посмотрите на функцию GetItemLoc():
Лучше использовать GetItemX/Y напрямую, тогда мы вызовем меньше функций, об этом мы говорили ранее.
Вернёмся к старой функции Trig_Unit_Dies_Actions и заставим её использовать реальные.
Так как локация - это координаты X,Y (у неё есть ещё и значение Z, но нам оно не нужно), нам понадобятся 2 реальных.
Нам нужно поменять здесь всего 3 вещи:
- GetRectCenter(). Эта функция просто вызывает 2 другие - GetRectCenterX/Y. Вызовем их напрямую.
- ReviveHeroLoc(). Это Native, но лучше использовать ReviveHero, которая напрямую берёт координаты X и Y.
- PanCameraToTimedLocForPlayer(). Смотрите, что мы здесь нашли. Похоже, мы что-то забыли оптимизировать в Правиле 1. Это говорит нам о важном моменте: изучите все функции, которые вы используете, даже если в конце их имени нет BJ, они могут быть функциями из Blizzard.j. Поэтому мы напрямую обратимся к этой функции и используем значения Х и Y напрямую.
Вот то, что у нас получится:
Итоги.
Используйте как можно меньше вызовов функций.
Используйте Native-функции.
Не раскрывайте очень большие функции.
Задавайте значение переменной разу после её объявления.
Используйте координаты х и у везде, где это возможно.
Не утверждайте, что фунция - Native, если у неё после названия не стоит BJ.