Страницы

среда, 22 декабря 2010 г.

Иерархия объектов

Сейчас тема посложнее и гораздо объёмнее. В моем текущем проекте необходима иерархическое, наследственное поведение объектов. А что это такое?


Допустим у нас есть юниты. Есть войны, а есть работники. У них есть что-то похожее, оба они ходят, у обоих есть параметры «Здоровье», «Команда», «Опыт». Смотрите:


И нам приходится писать код и там, и там. Причём на половину он идентичен. Как же этого можно избежать этого бессмысленного повторения? А для этого в конце 60-х придумали такую штуку как Объектно-ориентированное программирование (в дальнейшем ООП). Саму суть я сейчас расскажу, но если вы хотите углубиться вам придется почитать информацию по ссылочке. Так вот, плюшки этого ООП реализованы в ГМ (не полностью, конечно, но есть). Смотрим:


Заметьте как оба объекта «похудели». Использовать этот способ несложно, просто нужно в свойствах объекта выбрать объект в свойство Parent (Родитель), в нашем случае выбираем объект «Существо» (а сам объект в этом случае будет «Потомком» «Существа»). Но это ещё не всё. Не менее важное действие: теперь надо определить какие события, в каких частях, и в какой последовательности будет вызываться родительский код. Делается это функцией event_inherited(); (вы также можете найти это действие кнопкой, рядом с кнопкой скрипта, там будет надпись «CALL EVENT», советую пользоваться именно этим способом). Грубо говоря, это тоже такой же скрипт как и другие (ничего не возвращает), но вызывает весь код родителя в таком же событий (если было вызвано в Create «Война», то будет исполнено Create «Существа»). Всё, теперь оба объекта и «Войн», и «Рабочий» имеют все параметры и действия своего родителя «Существо», но и отличаются друг от друга. Важно отметить, это происходит не автоматически, а по вызову event_inherited();, это важно. Если у родителя есть Create и в нём есть переменные нужные для потомка, то в Create потомка нужно вызвать event_inherited();. Причём, если в Create потомка используются переменные родителя, то нужно сначала вызвать event_inherited();, а потом исполнять Create, иначе будет ошибка, переменных-то ещё нет. И тут возникают, иногда, забавные случаи, но об этом позже.

Далее допустим нам понадобился маг и новые параметры для боевых существ:


Но стойте! Мы же работаем методом ООП! Выделим общие свойства в новый объект «Боевой» и вуаля:


Вот такая у нас получилась структурка. Кстати, вы можете заметить как направлены стрелки - это показательно. Потомок вызывает событие родителя. Здесь важно понять, что если Войн вызывает событие Create родителя это ещё не значит что «Боевой» вызовет Create Существа (если вы так запрограммировали или если забыли поставить этот чудесный скрипт) и у Война может не оказаться переменных «Существа» - «Здоровье», «Команда», «Опыт». Будьте внимательны, либо тщательно продумывайте иерархию (желательно с подобными зарисовками, если иерархия сложна). Зачем же это надо? Всё просто, мы получаем несколько плюшек в следующих вещах:
  • Уменьшается объём кода. Причём тем сильнее, чем больше объектов одного родителя

  • Теперь чтобы изменить поведение всех объектов определённого родителя, то достаточно изменить всего 1 объект. Если потомков, допустим 10, то ваша работа ускоряется в 10 раз! Да и вы получаете прекрасный инструмент для гибкого изменения поведения потомка.
О плюшках поговорили, а теперь заикнусь о проблемах (а как же без них? ;) ).

При наследовании все данные нужно инкапсулировать по максимуму.
Что такое инкапсуляция? Вкратце, это возможность взаимодействия с объектом через его открытый интерфейс. Например, самый банальный пример, вы пользуетесь туалетом? Наверняка. А знаете как оно устроено? Наверняка, нет. Но даже не зная устройства унитаза вы им пользуетесь через открытый интерфейс - ручку слива воды. Нажали, дёрнули и вуаля! Объект используется, работает на нас. Или вот про компьютер: вы знаете что происходит в компьютере просто при нажатий кнопки? А происходит много чего, но вам это не важно и не нужно, вы знаете как работать клавиатурой и знаете что она делает и это ваш открытый интерфейс, через него вы работаете.
Т.е. всё что используется в родительском объекте должно оставаться там и только там. Чем меньше связей, тем лучше (тем безопаснее интерфейс), в идеале 0 связей. Но так не получается, мы используем свойства и функций родителя в потомке. Допустим опыт. У каждого объекта своё понятие «набирания опыта». Рабочий получает опыт за работу, Войн за махание мечом на тренировках и поле боя, а маг ещё и за учёбу в библиотеке. Принцип инкапсуляции предлагает нам сделать общий интерфейс. Что это такое на языке GML? Нам необходимо ввести скрипт получение_опыта(кол-во опыта); (настоятельно рекомендую запихнуть в папку с названием того родителя к которому относится) В скрипте строка начисления опыта поставим банально exp += argument0; Теперь в потомках этого родителя ставим этот скрипт.
если(войн ударил противника)
получение_опыта(50);
если(войн ударил своего ИЛИ получил урон)
получение_опыта(-5);
В этом случае, если вы решите отключить опыт или изменить его действие (по каким-то причинам), вам необходимо изменить 1 скрипт (просто удалить оттуда код или изменить его). Если же вы делаете просто кодом, не через скрипт, то тут возникнут проблемы. Во-первых, нельзя будет просто так отключить «Опыт», иначе вся игра будет падать под ошибками отсутствующих переменных. Во-вторых, нужно будет пройти все места и изменить действие набора опыта, что может оказаться довольно сложной задачей. Если игра готова, а производительность со скриптами не устраивает, то все скрипты можно развернуть, а вот обратное будет сделать тяжелее.

Проблема вторая, гораздо сложней. Она уже касается действий. Допустим вам надо чтобы рабочий, маг и войн находили путь к проблеме. Добавим остроты, уровень содержит не выпуклые многоугольники, поэтому необходим сложный алгоритм типа А*. Плюс нам необходимо находить путь достаточно быстро чтобы не подвешивать игру, но и чтобы быстро находить путь до быстро передвигающейся цели. Нетривиальная задача, да? Да-да, так и есть. Решение на текущий момент ищется, но наметки на решение есть.
Сделать что-то вроде шлюза. Объект потомок даёт команду «Нужно переместится в точку (x; y)», объект родитель (в нашем случае, «Существо», смотрим на схему, ходьба именно в этом объекте) проверяя каждый шаг точку назначения, если она оказывается не пустой, то идёт проверка на видимость. Если объект в прямой видимости, то идём методом потенциальных полей (mp_potential_step(_x, _y, _speed);), иначе строим А* путь и идём по нему. Можно устроить дополнительные проверки: если впервый раз строим путь А*, иначе если нужный объект сместился на большое расстояние. Если объект сменился, все настройки поиска сбрасываются.

А проблема заключается в следующем, как сделать так чтобы весь код отвечающий за ходьбу был в родительском объекте? В принципе, попробую поискать решение проблемы в первой проблеме через инкапсуляцию. Т.е. если мы используем шлюз, мы используем в потомках простой скрипт установки места назначения или объекта. Такой скрипт мы можем запихнуть в любой потомок, а весь код останется только родителе. Если же я решу использовать вместо А* другой алгоритм, усовершенствовать его или написать полностью свой мне надо будет изменить код в родителе и всё! Потомки ничего не знают про А* и алгоритмы движения, они говорят родителю «Шеф, трогай туда», а родитель уже думает как туда доставить. Инкапсуляция!

Заключение:
Наследственность в ГМ реализована не полностью, но то что есть даёт огромный простор для творчества, ускоряет и упрощает разработку в несколько раз! Пользуйтесь этой интересной фичей. Тем более что наследственность можно реализовать в каждой игре. В каждой из них есть базовый объект (Родитель), например, монстр, твёрдый объект, оружие, и есть множество различных потомков.

3 комментария: