Warcraft 3: Голодные игры

Тяжело найти игру, которая пыталась бы так экономить память и процессорное время, как  WC3. Ну серьезно, здесь кэшируется всё, что используется хотя бы один раз. Разработчики хотели добиться такой плавности, чтобы даже на чертовом Pentium 2 игра летала. Конечно, это не было указано в минимальных требованиях, но попробовать всегда можно.



Оптимизация, особенно в сравнении с каким-нибудь проектом уровня  Dota2, поражает. Пока творение на Source-движке отжирает память аки голодный Chrome или старая Лиса, требуя 1 Гб оперативной памяти только для загрузки главного меню,  WC3 просит ~500 Мб на меню и всю карту целиком. Это уже не говоря о скорости обработки информации — пока  Source в несколько ядер бездумно обрабатывает отрисовку шапочек и умудряется лагать,  WC3 держит 200+ юнитов на экране и не особо этим утруждается. Конечно, последнее зависит и от рук автора карты — в том же LTD наплыв крипов вполне успешно ложит производительность. Но старые добрые башенные защиты доказывают, что сам  варкрафт с этим справляется на 5+.

Здесь — о том, какой ценой были достигнуты подобные успехи, и к чему это привело.


Кэширование базовых параметров юнита

В жизни большинства юнитов есть как минимум два повторяющихся действия — атака и каст заклинания. Оба действия имеют пре- и пост-анимации, а также четко обозначенное время выполнения.

Атака

Атака  Treant требует 0.6 секунд с момента попадания цели в радиус атаки. Этот показатель множится на скорость атаки, поэтому, теоретически, может быть разным в каждом кадре. Поэтому скорость атаки регулярно пересчитывается — юнит будет атаковать с той скоростью, которая у него есть, а не которая в кэше.

Но у атаки есть другое статичное свойство — дальность. Фактически, она ведь не меняется с момента начала атаки: тип атаки уже выбран, корректность цели проверена. Так почему бы не закэшировать это значение, подумали  Blizzard, и сделали это. Юниты проверяют дальность своей атаки лишь один раз — при выборе цели. Приказы «атаковать», направленные на ту же цель, которая уже атакована приказом ранее, не обновляют кэш — ведь цель уже атакована, зачем пересчитывать?

Проблема в том, что в этом сценарии никак не учитывается потенциальное изменение дальности атаки при изучении апгрейдов или смены формы героя. Отсюда — приколы вроде знаменитого в узких кругах бага  Troll и  Terrorblade. Ему подвержен и  DK, просто он намного меньше страдает от него из-за активных абилок. Кэш обновится при следующей атаке, но эта конкретная атака, которую юнит хочет сделать сейчас, будет производиться с того расстояния, которое было закэшировано в момент получения приказа.



Кэширование происходит в момент отдачи приказа. Если нажать «атаковать» в ближней форме, а затем превратиться, герой побежит к цели вплотную — потому что в буфере находится старая дальность атаки. Даже повторный клик по той же цели ничего не изменит. Нужно прервать выполнение текущего приказа и отдать его заново — тогда в алгоритмы попадет обновленная дальность, т.к. предыдущий приказ не был атакой и нужно всё пересчитывать заново.

Каст

Время, которое нужно юниту для сотворения заклинания (дальше (и везде) — каст), четко указано в свойствах юнита и никогда не меняется. Ничто не может изменить этот параметр. Точно так же не меняется и время окончания каста, когда каст уже сделан, но юнит еще машет руками или ногами. Оба значения кэшируются в момент, когда юнит изучает заклинание, и в дальнейшем используются именно из кэша.

Прочитав один раз при изучении  Teleportation, что у  Furion каст тайм 0.5, а бэксвинг (бессмысленная анимация после каста) 0.97, игра будет использовать эти числа при каждом касте телепорта. Более того, кэшируется даже время каста самого заклинания. Например,  Teleportation имеет каст-тайм в 2 секунды. Сделать прогрессию, например, чтобы этот каст-тайм снижался до 2/1.5/1/0.5 за уровень, невозможно — всегда будет использоваться значение 2, независимо от того, что говорится на следующих уровнях.



Кэш, созданный однажды для юнита, не сбрасывается и живет вечно, до закрытия карты. На этом основано персональное изменение каст-таймов и бэксвинга для некоторых заклинаний, например — для отмены бэксвинга  Furion после телепортации и бэксвинга  Storm после ульты. Когда игрок пытается выучить эти заклинания первый раз, герой на мгновенние превращается в другого юнита с бэксвингом 0. Затем ему выдают заново заклинание и превращают обратно. В результате игра кэширует 0 для этого юнита и использует его в качестве бэксвинга.

Эта фича была бы еще полезнее, если был бы метод менять параметры без превращений. Превращения считаются активным действием и выводят юнита из невидимости, например.

Регенерация

У каждого юнита есть собственный показатель «базовая регенерация», один для маны, один для здоровья. Когда доходит до работы с ними, опять же, в дело вступает кэширование. На этом основан один небольшой абуз с  Alchemist, позволяющий долго оставаться с маной.

Когда юнит получает способность на регенерацию маны или увеличивает что-то, непосредственно влияющее на её регенерацию (в нашем случае — это интеллект), игра пересчитывает итоговую регенерацию и сохраняет её в кэше. Пока процент скорости регенерации маны и интеллект не меняются, будет использоваться именно значение из кэша.



Полезно также знать, что регенерация всегда работает только до тех пор, пока у юнита не хватает здоровья или маны. Как только запас достигает 100%, регенерация отключается. Так игра экономит еще некоторое количество процессорного времени. Когда регенерация потребуется снова, будет произведен новый перерасчет.

Пример: Алхимик может подобрать  Mask Of Sobi, находясь в ульте, и получить +50% к скорости регенерации маны от 3 маны (базовое значение для формы 1 уровня), а не от 0.01 (базовое значение манарегена для всех героев, кроме  Techies (0.02)). Регенерация останется на 1.5 выше, чем обычно, до тех пор, пока не изменится количество интеллекта или не будет достигнут максимум маны. Нужно подобрать Sobi в форме — только при получении способности производится перерасчет. Аналогично, если ходить с Sobi, а потом включить ульт, мана будет восстанавливаться от базиса в 0.01, а не 3.

Кэширование приказов

Каждый приказ, существующий в игре, кэшируется при появлении в игре заклинания, его использующего. Я уже писал о том, как эта система серьезно подпортила один из релизов. Конечно, это огромное зло, притом тяжело сказать, насколько это решение оправдано — неизвестно, сколько процессорного времени уходит на высчитывание корректности приказа.

Кэширование строк

Вместо того, чтобы использовать обычные текстовые строки, Blizzard решили заносить их в собственную таблицу строк и доставать оттуда готовенькими по необходимости. Опять же, причины такого решения крайне неочевидны. В C++ какие-то сложности при генерации строк? Или это попытка упростить переводы игры и взаимодействие локализованных версий? Неясно.

Известно лишь, что за несколько минут игры в таблице строк накапливаются тысячи позиций, большинство которых присутствуют в игре всего лишь один раз и хранить их не нужно, на самом деле.

Подгрузка по необходимости

Ничего не подгружается в игру до тех пор, пока в этом не будет нужды. Когда карта загружается, достаются файлы ландшафта, что в ней используется. Затем — модели деревьев, травы и прочих декораций. Когда создается первый юнит, подгружаются все способности, что он имеет. Речь идет даже о самых базовых, вроде движения или Stop.

Это обеспечивает сохранение приемлемого уровня потребления памяти и относительную плавность игры. Но, если какая-то способность имеет кучу уровней, подгрузка такого массива, хочешь или не хочешь, займет время. Поэтому игра подлагивает, когда  SF качает пассивку на первый раз — подгружается 37-уровневый черепок со счетчиком душ.



Чтобы снизить влияние таких лагов на игру, я избавился почти от всех многоуровневых способностей, которые можно было заменить простыми 1-4 уровнями. Поэтому Allstars бегает ощутимо быстрее своего родителя.

Заключение

Наверняка в игре кэшируется еще больше мелочей, но они остаются «невидимыми» для игрока или не влияют на игру так сильно, чтобы это стало заметно.

13 комментариев

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.