WarCraft 3: Game Cache + JASS: нестандартное применение

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

Для начала приведем два самых распространенных заблуждения, связанные с кэшем:
  1. "Кэш не работает в мультиплеере!"
Это неверно. В мультиплеере невозможно сохранить кэш на диск, чтобы перенести данные на другую карту, поэтому основной метод его применения теряет смысл. Однако, в пределах одной карты кэш превосходно работает как структура для хранения данных.
  1. "Кэш хранится на диске, а переменные - в памяти, поэтому обращение к кэшу сильно тормозит игру!"
И это неверно. Кэш можно в любой момент записать на диск - действием Save Game Cache, однако он полностью хранится в памяти. Запись на диск нужна только для того, чтобы в другой игре мы могли прочитать сохраненную в кэш информацию - а в этой статье данный стандартный метод применения кэша не рассматривается.
Несомненно, в силу больших возможностей кэша по сравнению с обычными массивами, обращение к отдельному его элементу происходит медленнее, чем прямое обращение к переменной или элементу массива. Однако, отсюда вовсе не следует, что любое применение кэша будет ужасно тормозить игру, а следовательно, кэш никогда и ни за что использовать не следует.
Практика показывает, что 10 одинаковых параллельно работающих периодических триггеров, выполняющихся каждый по 50 раз в секунду, и при каждом выполнении совершающих по 17 операций с кэшем (в сумме - 8500 операций в секунду), игру сколько-нибудь заметно не тормозят. То есть, разницей во времени работы обращения к массиву и обращения к кэшу можно в подавляющем большинстве случаев просто пренебречь - остальная часть вашего алгоритма, как правило, будет затрачивать несравнимо большее время.

Основные возможности кэша

По сути, кэш - это безразмерный ассоциативный массив, то есть, массив, доступ к элементам которого осуществляется не по индексу (номеру), а по произвольному ключу-строке. Вернее, по паре строк: так называемым ключу миссии (mission key) и ключу записи (key). В обычных триггерах первый ключ называется категорией (Category), второй - меткой (Label). Максимальное число записей в кэше теоретически неограничено (предположительно, ограничено лишь объемом доступной памяти).
Стандартные функции для работы с кэшем предусматривают хранение там данных 4-х основных типов: строки (String), целые числа (Integer), вещественные числа (Real), а также логические значения (Boolean). Также можно сохранить и восстановить юнита со всеми его параметрами, однако, это нужно, в основном, для легкого переноса юнитов между картами, и здесь мы работу с этим типом рассматривать не будем.
С помощью так называемого "Return Bug" (RB) можно также в кэш сохранить значения любых ссылочных типов (это переменные для указания на конкретные игровые объекты: например, Unit, Point, Trigger, Special Effect и так далее..), преобразовав их к обычному целочисленному типу Integer. Для этого необходимо воспользоваться функцией следующего содержания:

function H2I takes handle h returns integer
return h
return 0
endfunction
Вызвав H2I, и передав ей переменную любого ссылочного типа, мы получим на выходе целое число, которое можно сохранить в кэш, как и любое другое.
Чтобы сохраненное значение преобразовать затем обратно в указатель нужного типа, необходимо для каждого из используемых типов написать отдельную функцию, принимающую целое число и возвращающую нужный нам тип, например:

function I2U takes integer i returns unit
return i
return null
endfunction
Перед выполнением любых операций с кэшем его необходимо проинициализировать. Сделать это можно, создав в редакторе переменную типа Game Cache, и создав следующий триггер:

Events
   Map initialization
Actions
   Game Cache - Create a game cache from cache.w3v 
   Set cache = (Last created game cache)
(имя файла кэша никакой роли не играет)
Далее будем считать, что кэш у нас создан, и работать с глобальной переменной cache - это единственная переменная, требуемая для работы с кэшем.

Применение 1: бесконечное число "custom value"

Как известно, в игре есть возможность каждому юниту или предмету сопоставить одно целое число - так называемый Custom Value. Это находит довольно активное применение в разнообразных картах, единственная проблема - что этот Custom Value всего-то один на юнита, а иногда хочется сохранить больше, и не обязательно только целые числа.
С помощью кэша эта проблема полностью решается - с помощью нехитрого приема любому объекту (не только юниту!) можно назначить сколько угодно параметров, и в качестве названий этих параметров использовать любые строки.
Идея состоит в том, чтобы в качестве 1-го из пары ключей (ключа миссии) в кэше использовать handle объекта - его уникальный номер в игре, а в качестве 2-го ключа - произвольную, выбираемую нами строку.
Реализуется это при помощи нескольких крайне простых функций:

function get_object_iparam takes handle h, string key returns integer
   return GetStoredInteger(udg_cache, I2S(H2I(h)), key)
endfunction

function set_object_iparam takes handle h, string key, integer val returns nothing
   call StoreInteger(udg_cache, I2S(H2I(h)), key, val)
endfunction

function get_object_rparam takes handle h, string key returns real
   return GetStoredReal(udg_cache, I2S(H2I(h)), key)
endfunction

function set_object_rparam takes handle h, string key, real val returns nothing
   call StoreReal(udg_cache, I2S(H2I(h)), key, val)
endfunction

function get_object_bparam takes handle h, string key returns boolean
   return GetStoredBoolean(udg_cache, I2S(H2I(h)), key)
endfunction

function set_object_bparam takes handle h, string key, boolean val returns nothing
   call StoreBoolean(udg_cache, I2S(H2I(h)), key, val)
endfunction

function get_object_sparam takes handle h, string key returns string
   return GetStoredString(udg_cache, I2S(H2I(h)), key)
endfunction

function set_object_sparam takes handle h, string key, string val returns nothing
   call StoreString(udg_cache, I2S(H2I(h)), key, val)
endfunction

function flush_object takes handle h returns nothing
   call FlushStoredMission(udg_cache, I2S(H2I(h)))
endfunction
(код используемой здесь функции H2I см. выше)
Каждой из функций записи - set_object_(i|r|s|b)param (буква в названии соответствует типу - integer, real, string, boolean; как сохранять другие типы - см. выше) передается 3 параметра: ссылка на объект, название параметра, и затем - само значение.
Чтение ранее сохраненной записи выполняют функции get_object_(i|r|s|b)param - такой функции передается 2 параметра - ссылка на объект и название параметра.
Функция flush_object, которой передается единственный параметр - ссылка на объект, нужна, чтобы удалить из кэша все связанные с указанным объектом значения. Например, если юнит умирает, нет смысла дальше держать в памяти всю сохраненную про него информацию.

Применение 2: триггерные заклинания

Польза от кэша неоценима при создании грамотных триггерных заклинаний.
Допустим, мы создаем триггерное заклинание, которое в течение некоторого времени что-то делает с юнитом, на которого его применили, например, наносит 50 единиц урона в секунду, в течение 15 секунд. Основных методов решения подобных задач два: первый из них - сделать все одним триггером, в котором сделать цикл от 1 до 15, где действием Wait сделать ожидание в 1 секунду. Такой метод весьма неаккуратен, потому что, во-первых, на малых промежутках времени (< 0.1 сек) Wait срабатывает неточно, во-вторых, во время любой паузы в игре этот Wait будет все так же срабатывать, что выглядит немного нелепо.
Второй (и правильный) метод реализации - с помощью отдельно создаваемого триггера с периодическим событием - этих недостатков лишен. Но, в этом случае нам понадобится где-то сохранять какие-то промежуточные (рабочие) параметры для этого триггера - в нашем примере это:
  • юнит, которому наносятся повреждения;
  • юнит, "от имени" которого наносятся повреждения (применивший заклинание);
  • счетчик срабатываний триггера, чтобы через 15 раз его остановить.
При передаче данных через глобальные переменные возникает много неудобств, если одновременно заклинание может одновременно применяться на нескольких разных юнитов - следует как-то отличать, какой триггер за какого юнита отвечает.
При передаче данных через кэш все максимально просто и удобно: никаких дополнительных глобальных переменных не требуется, и никаких случайных "пересечений" в работе нескольких триггеров никогда не возникнет.
Суть метода здесь в том, что в качестве объекта, к которому мы будем привязывать наши промежуточные значения, мы будем использовать.. сам триггер. Действительно: из триггера мы всегда можем получить ссылку на него самого - GetTriggeringTrigger() (в обычных триггерах - This Trigger), эта ссылка - своя для каждого триггера, и в то же время, она не меняется от запуска к запуску.
Пример реализации описанного в начале раздела спелла с помощью кэша:

// функция периодического триггера
function spell_damage_runtime takes nothing returns nothing
   local trigger t = GetTriggeringTrigger()
   // считываем передаваемые значения
   local integer time = get_object_iparam(t, "time")
   local unit caster = I2U(get_object_iparam(t, "caster"))
   local unit target = I2U(get_object_iparam(t, "target"))

   // условие выхода: цель умерла или же триггер отработал все 15 раз
   if time >= 15 or GetUnitState(target, UNIT_STATE_LIFE) <= 0 then
      // уничтожаем сам триггер и все связанные с ним записи, и прекращаем выполнение
      call DestroyTrigger(t)
      call flush_object(t)
      return
   endif
   
   // содержательная часть: наносим урон цели
   call UnitDamageTarget(caster, target, 50, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)

   // увеличиваем счетчик срабатываний
   call set_object_iparam(t, "time", time + 1)
endfunction


// функция "запуска" (инициализации) спелла
function spell_damage_launch takes unit caster, unit target returns nothing
   local trigger t
   
   // создаем и настраиваем новый триггер, который будет отвечать за работу спелла
   set t = CreateTrigger()
   call TriggerAddAction(t, function spell_damage_runtime)
   call TriggerRegisterTimerEvent(t, 1.00, true)
   
   // связываем с только что созданным триггером необходимые для работы значения
   call set_object_iparam(t, "caster", H2I(caster))
   call set_object_iparam(t, "target", H2I(target))
   call set_object_iparam(t, "time", 0)
endfunction
Ну и для запуска спелла, как обычно, создаем триггер:

Events
  Unit - A unit Begins channeling an ability
Conditions
  (Ability being cast) Equal to [специально созданная для этого заклинания способность]
Actions
   Custom script:   call spell_damage_launch(GetTriggerUnit(), GetSpellTargetUnit())
Вот и все, безглючно работающий для любого количества юнитов, и не использующий глобальных переменных спелл готов. Можете проверить и убедиться в том, что его работа игру не тормозит =)

Применение 3: автоматически удаляемые спецэффекты

Довольно часто возникает задача - создать в определенной точке графический спецэффект, а по прошествии некоторого времени - его удалить. Стандартный метод через Wait (он же TriggerSleepAction()) часто не подходит, если создание эффекта не должно прерывать выполнение остальной части триггера. К тому же, надо где-то сохранять и тащить за собой ссылку на создаваемый эффект, чтобы затем его можно было удалить.
Все эти действия удобно заменить вызовом одной функции, куда кроме параметров эффекта, передавать еще и время, через которое он должен быть удален, и, вызвав эту функцию, навсегда забыть про созданный ей спецэффект - он удалится автоматически.
Все это, конечно же, элементарно реализуется с помощью кэша. Код функций следующий:

function I2FX takes integer i returns effect
return i
return null
endfunction

function destroy_effect takes nothing returns nothing
   local timer t = GetExpiredTimer()
   call DestroyEffect(I2FX(get_object_iparam(t, "fx")))
   call DestroyTimer(t)
   call flush_object(t)
endfunction

function launch_effect_loc takes string modelfile, location loc, real timeout returns nothing
   local timer t = CreateTimer()
   call TimerStart(t, timeout, false, function destroy_effect)
   call set_object_iparam(t, "fx", H2I(AddSpecialEffectLoc(modelfile, loc)))
   call RemoveLocation(loc)
endfunction

function launch_effect_unit takes string modelfile, unit target, string attachpoint, real timeout returns nothing
   local timer t = CreateTimer()
   call TimerStart(t, timeout, false, function destroy_effect)
   call set_object_iparam(t, "fx", H2I(AddSpecialEffectTarget(modelfile, target, attachpoint)))
endfunction
Первые 2 функции - служебные, а функции launch_effect_loc и launch_effect_unit дублируют стандартные функции AddSpecialEffectLoc и AddSpecialEffectTarget - создание эффекта в точке и на юните соответственно, но им передается дополнительный параметр - время жизни эффекта.

&nbsp;

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

Просмотров: 11 968

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


Chel5 #1 - 11 лет назад 3
Хорошая статья.
Coffin666 #2 - 10 лет назад 3
Статья Куль!!! Респект Автору
TGA #3 - 10 лет назад 3
респект. наконецто понял SCV
Freezen #4 - 9 лет назад 3
Кто-нибудь может обьяснить чем отличается наш SCV от западного LHV(Local Handle Variables)?
TRILOGY #5 - 9 лет назад 3
круто
BioAleks #6 - 8 лет назад 1
Хорошая статья, однако надо знать джасс чтобы изучить её.
___ydav___ #7 - 8 лет назад 3
С помощью таких статей хоть по немногу начинаю понимать jass. Автору зачет !
TiM #8 - 8 лет назад 3
RB пофиксили в 1.24б, которая вышла недавно. Так-что теперь не стоит его использовать.
prog #9 - 7 лет назад 1
стоит... но сейчас это уже виртуозные танцы с бубном (для непосвященных) а не банальное запихивание всего подряд в кеш ;)
хотя теперь есть альтернатива в виде хеш таблиц...
но мне, например, некоторые вещи по-прежнему удобнее делать связкой кеш+массивы+ф-ция получения хендла
ScorpioT1000 #10 - 5 лет назад 1
Следует заметить, что в последних версиях Warcraft III Return Bug был исправлен.
Подробная информация и решения в теме на форуме, в разделе Jass.
Теперь используется хеш-таблица
map_maiker #11 - 5 лет назад 1
статья гуд