Главная » Статьи » Создание карт » JASS / AI скрипты учебники

Осваиваем Jass: Глава 11, Глава 12, Глава 13.

24-04-2024» 11. RETURN BUG (RB)
Прежде всего, нужно рассказать немного о том, что собой представляет компьютерная память. Читатель, может быть ты и так это знаешь, но на всякий обрисую картину. Все без исключения игровые объекты - юниты, строки, числа - все это находится в компьютерной памяти. Память представляет собой просто последовательность идущих подряд ячеек, в каждую из которых записано определенное число. При помощи чисел можно закодировать все - и юниты и строки. Разные объекты занимают разное количество ячеек. Ведь чтобы записать объект юнит, нужно записать целую кучу информации о всех его параметрах. Это гораздо больше информации, чем то, которое несет какое-то значение типа integer. Вообще говоря разработчики war3 вряд ли хотели, чтобы картостроители могли работать с памятью, но они допустили при разработке один важный баг, которым научились пользоваться спецы по jass. Баг не позволяет менять содержимое ячейки памяти - но зато он дает возможность для любого игрового объекта найти номер ячейки памяти, в которую он записан, а также по номеру ячейки найти ссылку на объект.

К примеру, рассмотрим функцию

Код:
function FindHandle takes rect reg returns integer return reg return 0 endfunction
Что она по идее делает? В функцию передается параметр типа reg типа регион. Функция должна возвращать параметр типа integer. Вроде бы все нормально. Но почему-то присутствует два оператора return... Более того, первый из них говорит, что функция должна вернуть отнюдь не целочисленное значение, а наш регион reg.
Фишка в том, что war3 проверяет правильность функций по последнему оператору return. В этом последнем операторе записано:

Код:
return 0
Если бы не было этой записи, war3 выдал бы ошибку - т.к. наблюдается несоответствие типов. Должны вернуть integer, а возвращаем rect. Но у нас два оператора return. war3 смотрит - вроде все нормально, ошибок нет. 0 -ведь целочисленное число, значит его можно вернуть. И невдомек ему бедному, что хитрый jass-ер уже поставил до этого еще один return. И вот, возникает интересная задача - игра должна вернуть регион reg, как целочисленное число. По идее это бессмысленно, но тут и срабатывает return bug. Результатом работы функции будет число, равное адресу ячейки памяти, в которой содержится информация по региону reg.

Это первая часть бага. Есть и другая. Рассмотрим функцию:

Код:
function FindRegion takes integer i returns rect return i return null endfunction
Все аналогично. Функция должна вернуть регион, а вместо этого мы передаем ей параметр типа integer. Результатом будет то, что игра вернет регион, записанный по адресу i. Если конечно по этому адресу действительно записан регион. Впрочем, если не записан, это не будет ошибкой. Ошибка может возникнуть, если попробовать обратиться к недоступному участку памяти. Но это произойдет, разве что если ты будешь экспериментировать с памятью war3.

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

Какое может быть применение у return bug (RB)? Ну например, для наших функций. Зачем нам может понадобиться узнавать номера для регионов? Я этому сходу нашел следующее применение. В редакторе worldedit, к сожалению, невозможно простым и быстрым способом занести созданные регионы в массив. А ведь иногда бывает нужно. Скажем, на твоей карте несколько сотен регионов, в которых должны появиться одни и те же монстры и отправиться в патруль в следующую точку. Проще всего делать это через массивы с циклами. Но заносить вручную сотни регионов в массив - тоже приятного мало.
А я сделал предположение, что регионы, которые мы создаем в редакторе, занимают соседние ячейки памяти. Т.е. если создали мы первый регион - он попал в ячейку X, тогда следующий регион попадет в ячейку X+1 и т.д. Т.е. если мы знаем номер первого региона и остальные регионы создавали в определенном порядке, мы можем при помощи RB найти все остальные регионы и занести их в массив на автомате.
Итак, сначала при помощи функции FindHandle мы находим номер первого региона, затем при помощи функции FindRegion в цикле заносим все остальные регионы в массив. Элегантное решение нудной задачи :). Рекомендую посмотреть пример return bug, в котором реализованы все функции, описанные выше.

Но конечно RB имеет гораздо более ценное применение. Дело в том, что все ячейки памяти имеют сквозную нумерацию. Т.е. благодаря RB мы можем сопоставить всем объектам их номера (точнее номера ячеек, которые они занимают в памяти). И все эти номера будут уникальны. Совпадений не будет. Эту нумерацию можно использовать, чтобы... Впрочем, это тема другой части :).

» 12. Тип Handle
Читатель, кроме всех тех типов, к которым ты привык в редакторе переменных, существует еще один тип – handle. Это очень специфический тип - что-то типа универсального указателя на объекты. Нас он интересует с позиции его применения. К примеру, если ты сделаешь функцию:

Код:
function <Имя> takes handle h returns <...>
в аргументе указан тип handle. Так вот, в эту функцию в качестве параметра можно передавать ЛЮБОЙ игровой объект. Хочешь, посылай в качестве аргумента юнит, хочешь - регион, хочешь - точку и т.д. В связи с тем, что мы уже описали return bug, мы можем использовать его, чтобы найти ячейку в памяти, на которую ссылается handle:

Код:
function H2I takes handle h returns integer return h return 0 endfunction
Это дает нам определенное преимущество. Вот в прошлой статье мы рассмотрели функцию:

Код:
function FindHandle takes rect reg returns integer return reg return 0 endfunction
Для региона находили номер. А как насчет других объектов? Юнитов, точек и пр.? Для каждого создавать свою функцию? Можно конечно, но используя тип handle мы значительно упростим задачу. Функцию H2I, рассмотренная чуть выше, предназначена как раз для этой цели. H2I(O) - и есть номер ячейки в памяти, которую занимает объект O, каким бы ни был этот объект. Напишешь

Код:
set i = H2I(u)
- в i попадет номер для юнита u. И т.д.
Вот такой универсальный способ находить номер объекта. К сожалению, такого же универсального способа, чтобы по номеру определять что за объект в нем записан - нет.

»13. Система Super Custom Value (SCV) или RB+cache
Читатель, мы вплотную подошли к системе SCV. Эта система, которая позволяет сопоставлять ЛЮБОМУ игровому объекту - либо другой объект, либо какое-то значение (или даже массив). Наподобие custom value, но значительно более универсальная. Значение этой системы трудно переоценить. Фактически, она позволяет упростить решение огромного множества задач, избавиться от глобальных переменных и создавать так называемые кешь-переменные прямо во время игры.

С чего тут начать. Пожалуй, с Кеша. Существует такая замечательная вещь, называемая кешь. Программисты называет такие структуры - ассоциативный массив. Кешь в war3 - это особый двумерный массив, в котором в качестве аргументов используются строки. Т.е. вводишь аргументами 2 строки, им сопоставляется значение. Можно сопоставить значение типа integer, типа real, типа string и типа boolean.
Как жаль, что в этот массив нельзя записать ссылку на юниты, предметы, способности и т.п. Стоп, а действительно ли нельзя? Или все таки можно?

Ссылку может и нельзя, но давай вспоминать, что мы узнали про RB. Каждому игровому объекту соответствует уникальный номер, число типа integer. Это число можно найти, и по этому числу можно найти объект. А ведь число типа integer может быть записано в кешь!

(*) Итак, если мы используем кешь не для переброски данных, а для хранения информации, то в качестве хранимой информации кешь способен записать указатели (номера) объектов.

Это первый важный вывод. А теперь подумаем, если мы в кешь можем сохранять объекты, можем ли мы при помощи кешь сопоставить какому-то игровому объекту какое-то значение? Игровой объект имеет свой уникальный номер. Номер есть число, но специальные функции позволяют перевести его в строку.

(**)Договоримся, если мы хотим сопоставить игровому объекту значение в кешь, то в качестве первого аргумента записи будем использовать уникальный номер этого объекта, переведенный в строку.

(***)Что касается второго аргумента кешь, то мы можем использовать его, чтобы дать нашему сопоставлению уникальное имя.

Сопоставь факты, отмеченные выше, и ты поймешь идею SCV.

Рассмотрим функцию вида:

Код:
function set_object_iparam takes handle h, string key, integer val returns nothing call StoreInteger(udg_cache, I2S(H2I(h)), key, val) endfunction
Эта функция предназначена, чтобы сопоставлять любому объекту параметр типа integer. Аргументами выступает ссылка на объект handle h, строка key - имя сопоставления и переменная val типа integer - это число, которое мы сопоставляем объекту.
udg_cache - это переменная типа кешь - специальный кешь-файл создается в самом начале игры.
В функции единственное действие:

Код:
call StoreInteger(udg_cache, I2S(H2I(h)), key, val)
Это обычная команда занести значение в кешь.

Для записи в кешь, нудно передать 2 строки-аргумента. Первая строка:

Код:
I2S(H2I(h))
Разберемся подробнее. Здесь написана функция внутри функции. H2I(h) - мы уже рассмотрели выше. Она вернет номер для объекта, переданного через переменную h. Вторая функция I2S(...) - это обычная варкрафтовская функция перевода числа в строку. Итак, вся конструкция в целом приведет к тому, что первая строка - это переведенный в текст уникальный номер объекта.
Вторая строка key - это строка, которую заполняет сам пользователь, давая имя сопоставлению. Параметр для записи val.

Итак, если у тебя есть юнит u и ему нужно сопоставить число 10, то можно использовать команду:

Код:
call set_object_iparam(u, "int", 10)
имя сопоставления "int".

Отлично! Как делать запись мы выяснили. А можно ли эту запись прочитать обратно? Да! Во-первых, для удобства создадим вторую функцию:

Код:
function get_object_iparam takes handle h, string key returns integer return GetStoredInteger(udg_cache, I2S(H2I(h)), key) endfunction
Она похожа по структуре на предыдущую, только аргументов на один меньше. Это потому, что функция нужна не для записи значения в кешь, а для чтения значения из кеша.

Код:
return GetStoredInteger(udg_cache, I2S(H2I(h)), key)
Т.е. наша функция вернет значение выражения GetStoredInteger(udg_cache, I2S(H2I(h)), key) . А что это за выражение? Стандартная функция для чтения из кеша. В качестве первой строки указывается уникальный номер объекта, переведенный в строку. Вторая строка - определена пользователем.

Итак, если мы хотим узнать, что записано в записи кеша "int" для юнита u, используем команду:

Код:
set i = get_object_iparam(u, "int")
Т.е. можно и записывать значения и читать их. Читатель, не замечаешь чего-то общего между нашими сопоставлениями и custom value? По сути, custom value - это тоже сопоставление, но менее универсальное, т.к. можно сопоставлять юнитам (и только юнитам) одно (и только одно) значение типа integer. А при помощи SCV можно сопоставить что угодно и чему угодно. Поэтому я называл эту систему Super Custom Value (SCV) , а сопоставления-записи - для краткости cv.

А как сопоставить юниту u - другой юнит u2? Очень просто.

Код:
call set_object_iparam(u, "int", H2I(u2))
Мы записали в параметр "int" уникальный номер u2.

Этот номер мы можем прочесть обратно. Проблема лишь в том, как при помощи этого номера получить ссылку обратно на u2. Для этого в SCV есть специальные функции.

Код:
function I2U takes integer i returns unit return i return null endfunction
и

Код:
function get_object_uparam takes handle h, string key returns unit return I2U(GetStoredInteger(udg_cache, I2S(H2I(h)), key)) endfunction
Первая функция по уникальному номеру возвращает сам юнит, вторая сделана для простоты - она читает уникальный номер из записи в кеше и при помощи первой функции возвращает ссылку на этот юнит.
Так что, если нужно прочесть какой юнит записан в cv "int" для юнита u, используем команду

Код:
set u2 = get_object_uparam(u, "int")
Вот и все. Остальное все по аналогии. Есть и другие функции для сопоставления чисел real, строк, флагов boolean. Есть функции для нахождения не только юнитов по их номеру, но и других объектов - точек, регионов, спецэффектов и др.

Есть правда еще одна функция

Код:
function flush_object takes handle h returns nothing call FlushStoredMission(udg_cache, I2S(H2I(h))) endfunction
- она позволяет быстро отчистить все записи кеша, относящиеся к какому-то объекту.
Скажем, собираешься ты удалить юнит u. Для того, чтобы cv этого объекта не занимали место в памяти, когда объекта уже нет, пишешь команду:

Код:
call flush_object(u)
Все эти функции в сумме вмещаются на 1-1.5 экрана. Переносить систему из сценария в сценарий - элементарно. Просто копируем код, создаем переменную cache и при событии Map Initizlization создаем кешь-файл.

Читатель, попробуй представить себе все возможные способы применения SCV. Вспомни примеры, которые мы рассмотрели ранее. Может быть есть способ что-то сделать проще, быстрее и надежнее ?

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

Освоив SCV ты поднимешься на следующую ступень мастерства.

К статье прикреплен файл: return bug.w3x

Категория: JASS / AI скрипты учебники | Добавил: imDarkCount (24-04-2024 в 06:28:15)
Просмотров: 2545 | Рейтинг: 3.1/7
Всего комментариев: 0
ComForm">
avatar
Рейтинг@Mail.ru
Яндекс.Метрика

Copyright © 2010-2017
Вакансии :: Контакты
Мобильная версия сайта