WarCraft 3: 10. Оптимизация: утечки памяти

Осваиваем jass (0-1)

10. Оптимизация: утечки памяти

Читатель, некоторое представления на эту тему ты уже имеешь. Например, известный факт, что если не удалять созданные спецэффекты, то игра через некоторое время начнет сильно тормозить. Поэтому, даже если спецэффект мгновенного действия и через некоторое время уже не виден, его все равно нужно удалять. Почему так происходит? Потому что каждый спецэффект - это игровой объект. Когда мы создаем новый спецэффект, он попадает в память. Если его не удалять, то он останется в памяти до конца игры.
Аналогичная история с юнитами в ТД, Дотах или Аеонах. Умершие юниты должны быть удалены действием Remove Unit, чтобы не занимать место в памяти.
Оказывается, что такая же ситуация наблюдается и с остальными игровыми объектами. Предметы, декорации, регионы, точки, группы юнитов, плавающий текст, модификаторы видимости - все они с тем же успехом способны засорить память. Да, конечно объект типа точка занимает в памяти гораздо меньше места, чем юнит - в точку нужно записать только координаты X и Y, а в юнит - все его параметры. Но, вообще говоря, утечки в памяти склонны к накоплению. И постепенно игра становится все более тормознутой.
Показателен в этом смысле мой сценарий Air War - там где летает вертолет, управляемый стрелками. Передвижение вертолета реализовано примерно тем же методом, который рассматривался в прошлом сообщении, т.е. события с малым периодом. Но когда я выпустил первую версию, я понятия не имел о том, что нужно оптимизировать код и удалить утечки памяти.
Результат не заставил себя ждать. Через 2 минуты работы сценария, начиналось такое торможение, что дальнейшая игра не имела смысла. Я догадывался об утечках памяти, пытался сделать все проще и эффективнее. Удалось увеличить период игры с 2-х до 5 минут. Да и то, при условии, что играет 1 человек, а ведь чем больше - тем хуже. Разгадку утечки помог найти Alexey B.H.
Дело было в том, что для вычисления координат юнита я использовал стандартные операции worldedit с полярными координатами. А сделаны они, как оказалось, очень коряво. Каждый раз, когда ты используешь функцию point with polar offset, в игре создается новый объект типа точка. Обычно, даже не один, а два объекта. А теперь представь, что у нас работает периодический триггер, запускаемый 20 раз в секунду. И при каждом запуске, создаются новые объекты, которые не будут удаляться до конца игры. 40 точек в секунду, все равно что 80 параметров типа real. А если функцию полярных координат применять не один, а несколько раз в периодическом триггере (к примеру, чтобы вычислить положение камеры), да еще и вычислять координаты для 4-ех игроков - то... Получаем торможение игры.
Вариант устранения утечки подсказал Какодемон. К примеру, если мы хотим вычислить полярные координаты точки, полученной из точки p заданной положения юнита u при угле a равном углу между точкой p и какой-то точкой p2. Для этого можно воспользоваться действиями:
set p = GetUnitLoc(u)
set a = AngleBetweenPoints(p, p2)
call MoveLocation(p, GetLocationX(p) + 50 * CosBJ(a), GetLocationY(p) + 50 * SinBJ(a))
Теперь давай проанализируем. Сначала мы переменной p присваиваем положение юнита u. Следующим действием мы ПЕРЕМЕЩАЕМ точку p в другое место. А что это за место? Вообще, в чем суть команды MoveLocation?
Если бы мы написали
call MoveLocation(p, 10, 15) 
то координаты точки p стали бы (10,15)
Если бы мы написали
call MoveLocation(p, GetLocationX(p) + 50, GetLocationY(p) + 60) 
то точка p сместилась бы на вектор (50,60) относительно текущего положения.
А форма записи
call MoveLocation(p, GetLocationX(p) + r* CosBJ(a), GetLocationY(p) + r * SinBJ(a))
означает, что точка p сместится в точку, полученную пересечением окружности радиуса r и луча, проведенного из точки p под углом a. Т.е. это и есть по сути полярные координаты.
Если известно положение текущей точки (X,Y) и нужно найти координаты точки, полученной из текущей при смещении в направлении a на расстояние r – то ее координаты будут (X+r*cos(a), Y+r*sin(a)). Это все известные в математике факты.
Но в чем преимущество нового метода по сравнению со стандартной функцией вычисления полярных координат? Преимущество в том, что МЫ НЕ СОЗДАЕМ НОВУЮ ТОЧКУ, чтобы вычислить полярные координаты. Вместо этого мы ПЕРЕМЕЩАЕМ СУЩЕСТВУЮЩУЮ ТОЧКУ.
Есть конечно еще один нюанс. Обрати внимание на команду:
set p = GetUnitLoc(u) 
Присваиваем точке p положение юнита u. Но переменная со ссылкой на объект и сам объект - это разные вещи. До этого действия, переменная p была пустой. Объект точка с положением юнита не существовал. После действия, переменная p начала ссылаться на какую-то точку. Какой вывод? Игра создала объект типа точка и сделала на него сслыку в переменной p. Т.е. даже когда ты используешь элементарную функцию - определит положение юнита, в игре создается объект. И он тоже засоряет память - и это тоже станет заметно в триггерах с малым периодом.
Чтобы окончательно избавиться от утечки, необходимо применить действие для удаления объекта типа точка из памяти. В триггерах ты такой команды не найдешь. В jass эта команда выглядит следующим образом:
call RemoveLocation(p) 
Можешь посмотреть, как это сделано в примере, который я выслал в прошлый раз.
Ну, вроде с утечкой из-за точек разобрались. Но, увы, есть и другие виды утечек. Группы юнитов. Вообще-то есть переменные типа unit group. Это из той же серии. Переменная есть ссылка. А на что ссылается переменная unit group? На некий объект. Делаем выводы: объекты, на которые может ссылаться переменная unit group могут создаваться по ходу игры. И не только могут, но и создаются. И засоряют многострадальную память :).
Каждый раз, когда ты применяешь функцию pick every (unit in unit group) and do actions - ты в качестве unit group указываешь определенную функцию. Например, [units of type] или [units in range matching conditions]. Вот тут и скрывается зло. Именно из-за таких функций создаются объекты, которые будут торчать в памяти (кстати, во втором случае еще и точка может выплыть). И, как ты догадываешься, в триггерах с малым периодом - это означает торможение.
Так что если хочешь создавать, к примеру, огонь, который каждые 0.25 секунд наносит повреждения всем юнитам в такой-то области, нужно уметь бороться с утечками. А борьба будет просиходить по схеме:
  1. Создай переменную, например ug типа группа.
  2. Перед действием pick every unit нужно занести в переменную ug группу, которую мы будем использовать. Например: set ug = [units in range matching conditions]
  3. Во время действия pick every unit в качсестве группы указываем переменную ug.
  4. После действия pick every unit вставляем команду:
call DestroyGroup(udg_ug)
  • уничтожить группу из глобальной переменной ug.
Итак, подведем итог: в большинстве сценариев, утечки могут возникать из-за
  • юнитов (создаем много и не удаляем)
  • спецэффектов
  • функция для работы с точками
  • функций для работы с группами
Могут быть и другие варианты, но они встречаются реже.
Утечки особенно опасны, для триггеров с малым периодом.
Читатель, если удалить все основные утечки, то для 99,99% сценариев больше ничего не нужно оптимизировать. Но, как оказалось, существуют и другие виды утечек. Например, куда девается локальная переменная после того, как триггер кончил исполнение? На самом деле они продолжают сидеть в памяти. И хотя занимают они очень мало, но на протяжении длинной игры, их может накопиться порядочно. Чтобы этого избежать, имеет смысл обнулять локальные переменные после окончания действия триггера (по крайней мере, переменные объектного типа).
Ты наверное замечал уже это в примере, который я присылал. В конце триггера идут строки типа
set i = 0
set r = 0
set s = ""
А как обнулять переменные со ссылками на юниты, точки и пр.? Есть способ. Для всех объектных переменных ноль это null.
Т.е. мы пишем
set p = null
set u = null
и т.д. И переменные обнуляются.
Есть еще одна интересная возможность увеличить ресурсы памяти для длинных сценариев. Для этого нужно в какой-нибудь триггер с событием Map Initialization добавить команду:
call DoNotSaveReplay()
  • Эта команда заставит war3 не писать реплей игры. А значит, снизит ее загрузку.
Примечание. Мой друг картостроитель, прочитал про утечки и начал думать, что же с этим теперь делать? Глянул на свой проект и приуныл. Столько всего оптимизировать... Раньше для него не было проблемы утечек, а теперь пришлось думать, что с этим делать. А я думаю, может быть и не стоит так уж усердствовать? Во всяком случае не все сценарии требуют дотошно оптимизировать и удалять все утечки. В первую очередь, конечно, должны оптимизироваться сценарии для сетевой игры и сценарии, где есть триггеры с малым периодом. Остальные – надо смотреть по обстоятельствам. Если есть торможение, можно попробовать от него избавиться.
Вообщем, jass поможет вам решить проблемы, которые до его изучения не существовали. :)
Покончив с оптимизацией, мы наконец перейдем к одному из самых полезных аспектов jass.

Просмотров: 26 673

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


Liebendig1993 #1 - 7 лет назад -15
ваще ни о чем....обычная статья каких много...ничего нового
Rizhij_Nikitos #2 - 7 лет назад 15
Осваиваем Jass - если ты уже читал тонну статей по этой теме то зачем писать такие комментарии? Это гайд для новичков, которые не видели "статей каких много" - для них тут все новое.
Liebendig1993 #3 - 7 лет назад -4
Просто кудабы ни зашел, везде пишут ток одно и то же...для новичков полно статей на разных сайтах типа copy-past...я тож новичек в этом деле, и сделав все, что написано в этой статье у меня все-равно карта жрет по 50 кб в сек(((
Почему совершенно случайно узнаешь что triggering unit нельзя использовать после wait? Почему нигде не написано, что matching unit утечна? Ради интереса можете поискать в инете статьи об утечках...везде слизана одна и та же статья
Orses #4 - 7 лет назад 6
Liebendig1993, Ты должен делать всё сам а не скачивать готовое!
Нет ну ты представь себе ты зашол на сайт посмотрел про утечки, А кто-то эти утечки искал ГОД ИЛИ БОЛЬШЕ! (не понимал почему оно так и наконец нашол) и ты тут такой модный "А я карту крутую зделал без утечек (а имя того хто тебе сказал про утечки даже не вспомниш) !"
Тебе дана ОСНОВА! А всё остальное изучай САМ!
map_maiker #5 - 5 лет назад 4
гуд статья,+
Jusper #6 - 5 лет назад (отредактировано ) 0
вопрос такой
call DestroyGroup(udg_ug)
обязательна? или достаточно группе присвоить null?
16GB #7 - 5 лет назад 3
обязательно
DaeDR #8 - 5 лет назад 5
или достаточно группе присвоить null?
При обнулении ты просто очистишь переменную, но не уничтожишь группу. Т.е. на группу не будет ссылки, хотя она продолжит свое существование.
Jusper #9 - 5 лет назад 0
DaeDR, 16GB, спасибо =)
Тогда следующий вопрос: если я использую группу для отслеживания некоторых юнитов в отряде, и, скажем, не хочу ее дестрой, то мне для такой махинации нужно будет создавать отдельную группу?
DaeDR #10 - 5 лет назад 4
Если отслеживание происходит мгновенно (например: выбор и нанесение урона), то можно использовать стандартную bj-группу. А если для длительного отслеживания, то да, лучше отдельную.
Стратег #11 - 5 лет назад (отредактировано ) 3
а как массивы обнулять? Все перечислять или можно написать в общем виде?
DaeDR #12 - 5 лет назад 3
Только перечислением, циклом. integer, real, boolean и некоторые другие можно не обнулять (особенно, если это локальные переменные)
Storm_dll #13 - 4 года назад 3
Ничего не было сказано про
set bj_wantDestroyGroup = true
периодически это гораздо удобней. Тем более для оптимизации карты твоего друга, у которого уже много утечек памяти, чтобы не создавать перменные и тд, достаточно воткнуть 1 cs.
Buulichkaa #15 - 4 года назад 3
nvc123, не знал что мой мем будет пользоваться такой популярностью :D
Storm_dll #17 - 4 года назад 3
nvc123, больно надо. Ты еще скажи, что я бред написал? Вполне в тему.
nvc123 #18 - 4 года назад 0
Storm_dll, на дату статьи посмотри
с тех времён столько всего изменилось
Storm_dll #19 - 4 года назад 3
nvc123, я не спорю, но в комментах то же ничего не было сказано про это.
nvc123 #20 - 4 года назад 0
сейчас есть vjass,cjass,убрали рб,добавили хэш
по сути ты поднял старую тему которую все забросили
это называется археология или некромантия
Storm_dll #21 - 4 года назад 0
nvc123, я понимаю, что есть vJass и cJass, и не отрицаю, что то, что я написал, очень бонально, старо и просто, однако, скажем, если новичок начнет читать статью, это будет для него полезно.
Buulichkaa #23 - 4 года назад 3
nvc123, тебе ещё два до критической точки на 5 дней :D
CaptainFox #24 - 4 года назад 0
А разве различные программы типа МапОптимизер не делают подобную оптимизацию при прогонке карт через них?
Вестник Мондаса #25 - 4 года назад 0
CaptainFox,
Не думаю, тогда не было бы овер9к тем про оптимизацию кода. Кажется он всего-лишь удаляет лишнее барахло (неиспользованные переменные и т.п.).
JassJe #26 - 4 года назад 0
Интересно, раньше не использовал (пошел разбираться), автору спасибо за понятные статьи.
Buulichkaa #27 - 4 года назад 0
JassJe, понимаю, что новичок, но в дальнейшем постарайтесь не писать в старых ресурсах). про автора грех не cогласиться :D
ssbbssc #28 - 3 года назад -5
call DestroyGroup(udg_ug)
насчет этого пункта - спорно, потому что он заставляет половину моих триггеров с группами не работать!
nvc123 #29 - 3 года назад -4
это уже проблема твоих кривых рук
и если ты во всех триггерах используешь 1 группу то он правильно делает что недаёт триггерам работать
Это сообщение удалено