Для начала приведем два самых распространенных заблуждения, связанные с
кэшем:
- "Кэш не работает в мультиплеере!"
Это неверно. В
мультиплеере невозможно сохранить кэш на диск, чтобы перенести данные на другую
карту, поэтому основной метод его применения теряет смысл. Однако, в пределах
одной карты кэш превосходно работает как структура для хранения
данных.
- "Кэш хранится на диске, а переменные - в памяти, поэтому обращение к кэшу
сильно тормозит игру!"
И это неверно. Кэш можно в любой
момент записать на диск - действием 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"))
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 - создание эффекта в точке и на юните соответственно, но
им передается дополнительный параметр - время жизни эффекта.
В качестве примера к статье можно привести прикрепленную ниже карту; все
триггерные спеллы, на ней используемые, созданы исключительно по описывавшимся в
данной статье методам.