WarCraft 3: Приложение 1: проблемы Кеша и РБ

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

16.Приложение 1: проблемы Кеша и РБ

Читатель, на первых порах тебе важнее понять принцип работы SCV и научиться его применять. Но в реальности применение системы return bug + Кеш порождает определенные проблемы, которые я постараюсь рассмотреть в этом приложении.
1. Глюки нескольких запусков.
При запуске сценария мы создаем в памяти Кеш и делаем в него записи. Когда мы выходим из сценария, записи должны удаляться. Должны то должны, но иногда этого не происходит. И тогда при следующем запуске игры могут остаться записи, относящиеся к несуществующим объектам. Это чревато ужасными глюками. Я сам с этим столкнулся и долго не мог понять, почему глючит код, в котором не может быть никаких ошибок. И только проверив все самым тщательным образом, обнаружил, что записи не стерты.
К этому багу мне удалось найти противоядие. Вместо стандартной инициализации Кеша, я делаю следующее:
Create a game cache from SCV.w3v
Clear (Last created game cache)
Create a game cache from SCV.w3v
Set cache = (Last created game cache) 
Т.е. очищаю Кеш и инициализирую заново. После этого можно быть уверенным, что Кеш будет очищен.
2. Скорость скриптов.
Существует доказательство того, что 1 обращение к Кешу (чтение/запись) раз в 10 медленнее, чем обращение к переменным.
Для некоторых любителей оптимизации это стало поводом отказаться от Кеша и разрабатывать альтернативные системы при помощи масствов. В то же время, я не советую следовать их примеру. Да, я не исключаю, что кому-то из jass-еров удастся разработать более быстрые системы, но вряд ли получится сделать их более простыми, универсальными и доступными, чем SCV.
Практика показывает, что даже для такой карты как Дота применение Кеша вполне приемлемо. Если качество SCV вас устраивает, то нет смысла тратить массу времени и сил на разработку собственных систем.
Кстати говоря, скорость большинства внутренних функций war3 еще меньше, чем обращение к Кешу. Поэтому, если ваш скрипт работает медленно – 99% что проблема в алгоритме, а не Кеше.
Впрочем, кое-какие выводы для себя можно для себя сделать.
  1. Кеш лучше всего использовать в случаях, когда в этом есть реальная необходимость. Например, когда по ходу игры нужно одному объекту сопоставить другой объект. Если же у вас имеются данные, которые хранятся в течение всей игры, их лучше хранить в массивах.
  2. Старайтесь минимизировать число параметров, которые требуется передавать через Кеш в триггерах с малыми периодами.
  3. Если внутри одной и той же функции нужно несколько раз обратиться к одной и той же записи – поместите эту запись в переменную и обращайтесь к переменной.
3. Баг РБ.
Не очень давно был обнаружен баг внутри РБ (баг в баге). Суть в следующем: в результате определенных действий, игровой объект (чаще всего, юнит) может исчезнуть из игры. А записи в Кеше останутся. Например, если уже был запущен какой-то триггер, делающий отсроченный действия с этим объектом. Но еще до того, как отсроченное действие будет выполнено, в игре могут появиться новые объекты. И существует шанс, что игра запишет один из них именно в ту ячейку памяти, в которой раньше был записан старый объект.
Что получится? Игра должна была совершить определенное действие со старыми объектом, а произведет с новым. Потому что указатель на старый и новый юнит совпадают.
Подобная проблема вполне может возникнуть в АОСах. Допустим у вас есть триггерное заклинание, которое при помощи таймер, РБ и Кеша периодически наносит повреждения юниту-цели. Но вот, юнита-цель убивает какой-нибудь артиллерист. Юнит не просто умирает, а взрывается. Т.е. сразу после смерти он исчезает из игры. Но в АОСах постоянно создаются все новы юниты. И вот, создается новый юнит в той же ячейке, что и старый. И тогда мы получим, что периодические повреждения будут наноситься какому-то случайному юниту. Т.е. получили баг.
Как с этим багом бороться? Вообще-то если вы будете все аккуратно программировать, то баг просто не возникнет. Всегда можно отловить смерть юнита (в момент смерти юнит еще присутствует в игре). Можно сопоставить юниту все запущенные для него таймеры. При смерти юнита предусмотреть, чтобы с этими таймерами не возникало проблем. Затем, удаляем все записи Кеша, связанные с умершим юнитом.
Есть еще один прием. Дело в том, что новый юнит может занять место старого только при условии, что на старый юнит нет ссылок из переменных (неважно, глобальных или локальных). Можно использовать такой прием. Триггер, который сработает при смерти юнита:

local unit u = GetDyingUnit()
call PolledWait(60.0)
call flush_object(u)
set u = null
Т.е. любой умерший юнит попадает в локальную переменную на 60 секунд. После чего записи из Кеша отчищаются. Для большинства отстроченных действий 60 секунд вполне хватит, чтобы обнаружить, что юнит-цель уже мертв.
Кстати, можем получить и другую сторону этого бага. Даже если ничего не будет записано в пустую ячейку удаленного юнита, отсроченные действия могут не предусмотреть наличие юнита-пустышки там, где раньше был живой юнит.
Реальный пример. Я долго не мог понять, почему глючит моя триггерная аура. Технология ауры заключалась в том, что создавался периодический триггер. Ему сопоставлялась группа юнитов, на которых действует аура. В триггере нужно периодически перебирать юнитов из группы. За это отвечал такой блок:
    set k2 = CountUnitsInGroup(gr)
    set k = 1
    loop
        exitwhen k>k2
        set u = FirstOfGroup(gr)
        ...
        call GroupRemoveUnit(gr,u)
        set k = k + 1
    endloop
Но если в группу gr зачешется уже убранный из игры юнит – скрипт прекратит выполняться, когда дойдет до строчки set u = FirstOfGroup(gr). Точнее, он вылетит, когда, первым в группе окажется пустой юнит. При этом вы никак не сможете избавиться от этого глючного юнита. Его уже невозможно удалить из группы. Пришлось немного помучаться, пока я не нашел противоядие к этому багу:
    set gr2 = CreateGroup()
    call GroupAddGroup( gr, gr2 )
    set k2 = CountUnitsInGroup(gr2)
    set k = 1
    loop
        exitwhen k>k2
        set u = FirstOfGroup(gr2)
        ...
        call GroupRemoveUnit(gr2,u)
        set k = k + 1
    endloop
Т.е. мы делаем еще одну группу. В эту группу помещаем все юниты из первой группы (не надо бояться – глючный юнит невозможно добавить в новую группу, также как невозможно удалить его из старой группы). Далее перебираем не первую, а вторую группу.

Просмотров: 7 720

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


MOZGIII #1 - 7 лет назад 2
Очень опасный и непредсказуемый код... А что если в память варика вмешается какое-нибудь другое приложение и как-нибудь заставит варик сменить все адреса всех объектов, например сместить их? Сам по себе варик таким образом может и не упадёт, но вот скрипты ваши точно все косяками пойдут... Дота из=за этого очень много рушиласть до 24 патча на неофф пвпгн серверах
Zanozus #2 - 7 лет назад 0
"А что если в память варика вмешается какое-нибудь другое приложение..."
да легко, например оперативная память на компьютере переполнилась и все данные с оперативки перебрасываются в PagePool.
Да и какая теперь разница RB+SCV больше не существуют
Артас Менетил #3 - 5 лет назад 0
Теперь существует хэш-таблица, это по сути тот же SCV.
А вместо ReturnBug есть GetHandleID(handle)