Страницы

пятница, 6 мая 2011 г.

Абстракция

На текущий момент я доделываю поиск пути и хождение по нему. Так вот, возникла следующая проблема:
Нужна функция хождения с обходом препятствий, но встроенная mp_potential_step() уже не устраивает. К счастью, я заранее сделал функцию-обёртку СделатьШаг() и засунул туда одну единственную строчку mp_potential_step() с параметрами и использовал этот СделатьШаг() где мне надо было. Это может показаться глупо, зачем делать функцию из одной строчки? Но тут мы подходим к одному из столпов ООП: Парадигма абстракция.


Определение в Wiki:
Абстрагирование – это способ выделить набор значимых характеристик объекта, исключая из рассмотрения незначимые. Соответственно, абстракция – это набор всех таких характеристик.

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



Так зачем же нужная эта Абстракция? Теперь, если мне надо будет изменить действие СделатьШаг() со встроенной функций на какую-то свою, мне необходимо будет изменить лишь один скрипт в одном месте. И вся программа будет работать по новому с минимальными затратами. Удобно? Ещё бы. Минус то что мы теряем в производительности программы на вызов скрипта. Но здесь вы уже решаете самостоятельно данный вопрос что вам важнее.
Если мне захочется изменить поиск пути с волнового на А*, то пожалуйста! Меняйте реализацию скрипта НайтиПуть() и задача будет решена. Также с реализацией функций Пришли(), мы можем сделать вхождение в радиус, в прямоугольник, стоим ли мы в клетке цели или ещё какие варианты - это не важно, все будет работать отлично и так как вы хотите.

Абстракция - это что-то такое виртуальное, не существующее, но представляемое в своим воображении. Мы примерно знаем что с ней можно сделать и как использовать, но только примерно. Классический пример: фигура. Это слово обозначает абстрактный объект. Физически фигура не существует, потому что мы не знаем что такое, знаем только что любую фигуру можно вращать, перемещать, рисовать, конечно, только примерно, но знаем что можно. Как например, можно нарисовать фигуру? Что вы будете рисовать? И здесь вы будете спускаться к конкретике, т.к. вы знаете какие фигуры существуют. Допустим окружность, прямоугольник, треугольник и др. Это уже конкретные объекты с конкретными действиями, мы знаем конкретно и точно как переместить, вращать и рисовать треугольник и др. фигуры. Суть абстракций выделить общее из всех этих объектов.

Следующий пример, автомобили. Это абстракция. Автомобиль - это некий объект который может передвигаться с помощью человека. Что можно делать со всеми автомобилями? Крутить баранку, давить на газ/тормоз, ключ зажигания для включения двигателя. Это интерфейс. Зная его вы можете управлять абсолютно любым автомобилем! Не важно что у одно руль круглый, у другого эллипсный, у третьего он меховой, у четвёртого он справа и т.д. у каждого автомобиля есть руль и его можно вращать в стороны (если его нет, то это уже не автомобиль, а нечто другое, похожее, но другое). Допустим вы пересели с 9-ки на совершенно другой автомобиль, допустим, лексус. Вы также сможете им управлять, т.к. вы знаете интерфейс автомобилей. Это также может быть тойота, ламборгини и какая-нибудь китайская подделка, которая никому неизвестна. У всех них есть ключ зажигания, газ/тормоз и руль.

Теперь перейдем от машин к нашему коду (взгляните ещё раз на иллюстрацию в начале). См. скрипт СделатьШаг(), это некая абстракция. Что-то сделает шаг. Как оно это сделает? Это не важно, важно то что этот скрипт передвинет объект его вызвавший на шаг вперёд к цели. Реализация может быть любой: скрипт просто пустой (т.е. абсолютно ничего не делает), просто двигает, двигает к цели не учитывая препятствии, учитывая их или по методу потенциальных путей и т.д. Все эти скрипты как рули в машинах, они разные, но делают одно и то же дело, хоть и по разному. Это и есть абстракция. И смысл в нем такой как и писалось ранее:
  • Можно свободно поменять руль, он будет новый, другой, но машина как ездила так и будет ездить
  • Можно свободно поменять реализацию скрипта СделатьШаг() на любую другую, вся конструкция программы также будет рабочей

Чтобы сохранить общую работоспособность программы, нужно чтобы этот скрипт был работоспособным. Один вариант, если этот скрипт не содержит синтаксических ошибок, как и программа, тогда не будет фатальной ошибки. Второй вариант, если скрипт рабочий в логическом плане (т.е. выполняет свою функцию полностью) и остальные части программы, то все будет работать логически правильно. Вроде бы, все просто и логично, но делая простую схемку можно наткнуться на несколько непростых проблем. Синтаксические проблемы оставим, эти легко решаются, остановимся на логических. Опять см. иллюстрацию. Допустим, СделатьШаг() двигает объект вперёд объект на шаг, если нет препятствий на пути. Если препятствий по пути не встретится, то все будет рабочее и все будет хорошо. Но. Встреться препятствие как скрипт будет пропускаться, т.к. объект двигается, если нет препятствия, и тогда этот скрипт будет вечен (объект просто встанет как вкопанный до его конца). А раз он вечен, то и вся конструкция вечна, другими словами не добирается до выполнения, т.е. не то что нужно, либо не рабочая. Решение здесь, либо не учитывать препятствия, либо найти альтернативную свободную точку. Другой пример, более интересный. Скрипт СделатьШаг() рабочий, обходит препятствия, а функция Пришли выдает правду только тогда когда мы очень близко к цели, ближе чем длина шага. Т.о. если вдруг юнит немного смещен, то когда он будет подходить к цели близко, то скрипт Пришли не выдает правду, т.к. мы ещё далеко по его мнению, но шаг настолько велик, что мы перескакиваем цель и вновь оказываемся слишком далеко (типа как в сказке про кузнечика, который прыгал всегда очень далеко, но прыгнуть чуть-чуть он не умел. Тогда он смог попасть домой. И здесь точно также). Допустим мы стоим в 0, шаг 1, точка назначения 2, считается что мы пришли, если разница между целью и объектом меньше 0.5. Дойдем отлично через 2 шага (разница будет 0, что меньше 0.5). А вот если мы будем находится в 0.5, а не в 0, то мы никогда не дойдем (через один шаг будет 1.5, а на второй 2.5 и разница не меньше чем 0.5). Решений здесь тоже несколько: учитывать что мы пришли по ширине шага. Либо делать шаг минимум(расстояния до цели, длина шага). Обе проблемы реальны, взяты с моего проекта. Этот код находится в нескольких местах, но т.к. он абстрагирован в скриптах (т.н. функций-обёртки), то мне нужно поменять код только в одном месте и проблема решена. Или если мне нужно сделать новый тип хождения, то тоже нужно поменять только 1 место. Удобно, быстро.

Следующее. Интерфейс скриптов необходимо сделать ДО того как написан реальный код. У нас могут быть скрипты с названиями интерфейсов, но совершенно пустые. С синтаксической точки зрения, всё правильно, с логической нет, поэтому проект запуститься и будет работать, но будет работать не так как мы хотим. Пример, снова смотрим иллюстрацию. Каждый квадрат это скрипт. Вы их можете накидать чисто теоретически, как заглушки. В этом случае мы создаем интерфейс, но без реализаций, мы смотрим как скрипты подходят друг к другу (если вы читали мою книжечку о геймдизе про кабели, то имеете понятие.). На самом деле, создание интерфейса гораздо важнее, чем реализация. Но 99% новичков этого не видят, а потом у них возникают серьёзные проблемы и так же в 99% случаев просто забрасывают проект или в лучшем начинают сначала. Так вот, чтобы не начинать проект сначала, нужно заранее продумать проект и в частности интерфейс. На иллюстраций есть почти готовый интерфейс, но реализаций 0. Т.е. вы видите, что юнит должен совершать шаг, если он от цели далеко, но как он совершает шаг вы не знаете (обходит он препятствия или нет, прямо он идёт или по синусу и т.д.). Также вы не знаете как я нахожу путь, по волновому алгоритму или по А*. И это не важно, важно то что здесь у меня ищется путь, а тут юнит делает шаг. Такую схемку вы должны себе рисовать всегда, так вы будете лучше видеть вашу программу, а так же будете лучше видеть слабые места в программе. 

Теперь вернемся к теме. Важно ещё то, что эти функций должны быть связанны, а ещё лучше полностью инкапсулированы. Ясно что волновой алгоритм и А* работают по разному и что если связать ходьбу непосредственно с алгоритмом поиска, то он будет чересчур привязан и при подмене с одного на другой возникнут проблемы. Просто: функции поиска пути выдают некий список точек. Всё. Они ни с чем не взаимодействуют, они выдают готовый список точек. Не важно, правильный он или нет, он должен соответствовать списку точек. Далее, сама ходьба берёт этот список и идёт по нему. Ходьба и поиск пути не взаимодействуют с друг другом на прямую. Как и вы не идёте на завод за чипсами, а идёте к посреднику - в магазин. Здесь точно также: 
  • Скрипт ходьбы смотрит пуст ли список точек, если нет, то СделаемШаг(). Принимаем список точек
  • Нужно идти в точку, поиск пути строит список точек. Выводим список точек
Эти 2 алгоритма не взаимодействую друг с другом, они работают через посредника. Теперь нам не важно какой алгоритм в поиске пути (волновой или А*), главное чтоб он выдавал список и неважно какой алгоритм в СделатьШаг(), главное чтоб он принимал список точек. Они делают своё дело, как именно не важно.

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

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

Скрипт поиска пути. 
Абстракция: при использовании, все поиски пути возвращают список точек. Волновой или А* или ещё какой, они все вернут этот список.
Инкапсуляция: используем алгоритм и он возвращает список точек, как он это сделает не важно. Все алгоритмы используются через скрипт НайтиПуть(). Но А* делает это так, а Волновой вот так.

Update: Хочу ещё немного подробнее рассказать про разницу между инкапсуляцией и абстракцией.
Инкапсуляция: Возьмем известную функцию mp_potential_step(), допустим она занимает десяток строк, все из которых необходимы для работы. Представьте что у вас есть 5 объектов: во всех из которых где-то нужно использовать, а в некоторых нужно несколько раз. Если бы мы решили просто пихнуть в эти места, то изменять все это было бы очень трудно. Да? 5 объектов да минимум по 10 строк уже 50строк кода. Причём одинакового. Это хорошо что вы не забудете куда либо написать все 10 строчек из этого алгоритма! А если забудете? Для этого придумали штуку - инкапсуляция. Теперь вы, везде где нужно, используете функцию mp_potential_step(); Вы знаете что оно делает, но не знаете как именно. Да и это не важно, если все работает по вашему.

Абстракция: А теперь возьмем 2 скрипта mp_potential_step() и mp_linear_step(). Как мы видим обе эти функций выполняют одну и ту же работу - идут к точке. Но выполняют это по-разному. Поэтому мы можем объединить их в одну функцию - Идти(). И в эту функцию подставлять одну из тех что выполняют шаг. И мы выбираем, хотим ли мы чтобы шаг делался с обходом препятствии, либо без них, а может ещё как-то по нашему.

Вот и вся разница! Одно скрывает реализацию и выставляет открытый интерфейс как с mp_potential_step, а другое находит в них общее и использует это.

Это мощный инструмент ООП и очень рекомендуется его использовать, чтобы получить все плюшки. Вы можете его использовать в других местах, кроме ходьбы. Допустим стрельба, прыжок, ИИ и др. места. найдите общее в них и вынесите это отдельно. Затем вы их можете менять как хотите. Например, я нашёл что скрипт СделатьШаг делает одно и то же, несмотря на то что будет ли там mp_potential_step() или похожий алгоритм, поэтому я решил его абстрагировать. Почему? Потому что я знал что буду менять его на свой аналог и если бы я не абстрагировал этот скрипт, то пришлось бы его менять его в нескольких десятках местах, причём очень часто. А это мне совсем не хочется.

1 комментарий: