
Видеоподкасты по паттернам проектирования
Для всех любителей паттернов проектирования — видеоподкасты на английском языке (AS3).
Уже рассказывалось о посетителе, команде, хранителе и стратегии.
Можем заодно обсудить кто как использует паттерны в разработке игр. Я, например, одно время злоупотреблял синглтонами, сейчас вроде отошел. Вот такие простыни в коде встречались:
Ну и state machine, куда-же без нее.
Уже рассказывалось о посетителе, команде, хранителе и стратегии.
Можем заодно обсудить кто как использует паттерны в разработке игр. Я, например, одно время злоупотреблял синглтонами, сейчас вроде отошел. Вот такие простыни в коде встречались:
LevelTimer.instance.update();
MovementManager.instance.update();
Camera.instance.update();
HeroManager.instance.update();
PeopleManager.instance.update();
EnemiesManager.instance.update();
OrbsManager.instance.update();
CastleManager.instance.update();
PrisonsManager.instance.update();
BonusesManager.instance.update();
BombsManager.instance.update();
ShipsManager.instance.update();
Ну и state machine, куда-же без нее.
- +4
- qdrj
Комментарии (53)
Есть еще отличный сайт ActionScript 3 Design Patterns. OOP Techniques for Flash and Flex Developers.
Сам только начинаю разбираться с паттернами, потому что многие вещи получаются не оптимальными.
Было бы интересно почитать про ваш опыт: что и как используете, какие выгоды получаете.
Вот это было бы прекрасно)
Можете подробнее про композицию рассказать?
Читал про нее много, преимущества вкусные, но как практически применить еще не разобрался.
Пока читал про Декоратор, но есть сомнения насчет производительности такого решения.
Движок для игр Push Buttone Engine построен на компонентах, но хотелось бы начать с чего-то попроще.
1. Сделать так, чтобы группе кодеров было легче писать код вместе, чтобы была какая то типизация приёмов программирования, что существенно повысит их коллективный КПД — но инди-разработчикам это особо неинтересно.
2. Делать код более ошибкоустойчивым. Если приучить себя писать сразу «правильный» код, то при кажущемся изначальном громоздком стиле написания кода, можно сэкономить большое время отлова ошибок, особенно которые сложно отловить сразу.
3. Вот это очень важно для нас — при написании кода отделять абстрактный код, который может быть использован в дальнейших играх от кода, непосредственно связанного с конкретной игрой. Это может сильно сократить время написания следующих игр и улучшит в целом архитектуру кодирования.
Очень важно понимать зачем это всё нужно, потому что есть кодеры, которые просто изучают эти паттерны, а потом пытаются их прилепить в код, не понимая зачем и где это нужно.
Движок Push Button Engine — это ИМХО страх и ужас — явный примерно никому не нужного усложнения структуры движка приведшего к его абсолютной непопулярности. Но я в общем то непрофессиональный программист, может профы отпишутся...*)
Универсальный код это миф, можно разве что делать переиспользуемые компоненты, чтобы движок состоял из независимых модулей (компонент).
Разделение на компоненты — альтернатива множественному наследованию, только за это такой подход надо любить :)
Простейший пример:
1. базовый объект
2. объект, который обладает здоровьем
3. объект, который обладает магией
4. объект, который обладает здоровьем и магией.
Без компонент или множественного наследования — будут ифы или дублирование кода. Интерфейсы не помогут, потому что надо хранить данные.
Также компоненты позволяют динамически компоновать объекты, что не очень-то нужно, но прикольно.
Уменьшается связность. Классы знают только о тех классах, которые им нужны. Тоже нифига не надо для инди, но переписывать помогает.
Сам сейчас делаю что-то вроде компонентной системы. Переучиваться сложно, но надо когда-то начинать :)
И даже без базового объекта — так чтобы любой плагин мог включать другие плагины. Хотя это просто объединение базового с плагинами.
baseObj
objH наследует от baseObj
objM наследует от base
objHM наследует от objH и objM
А вот насчет плагинов можно поподробнее в общих чертах?
А про плагин присоединяюсь, интересно было бы посмотреть.
Правда, думаю, паттерн по-другому называется.
Вот все ругают ПБЕ, а покажите как просто и эффективно организовать компонентную архитектуру?
А что делать, если «очень хочется»?
Плагины (погуглил), по моему, немного не о том.
Плагины подойдут если «или здоровье, или магия».
Для композиции надо другое.
Компонентная архитектура в Unity3d сделана, насколько я могу судить по небольшому опыту общения с ней.
В GPG6 еще была статья, но вот как все это разруливается в реальных проектах, я бы с большим интересом посмотрел.
Вот общий пример:
1) Разбиваем систему на группы свойств. Это называем плагинами. Отличие от свойств в том что плагин может содержать кучу свойств и методов.
2) Выделяем общий интерфейс работы с плагинами и взаимодействия плагинов с контейнером.
3) Формируем базовый объект — контейнер по требованиям к системе (взаимодействие с другими контейнерами и прочими элементами системы).
На примере:
Приведенная система с жизнью и магией. Для жизни нет регенерации по умолчанию, для магии есть. Объект (контейнер) может обладать или не обладать магией (вообще от рождения) и должен обязательно обладать жизнью.
1) Свойства: жизнь, магия. Это очень упрощенные плагины, доведенные до свойств.
2) Здесь пространство для творчества, и завязано на п.3.
2.1. Работа с плагинами.
Надо получать их свойства. Делаем для этого метод GetProp(name:String):Object. Передавать имя или нет зависит от других решений для расширяемости и наглядности оставлю.
Надо что-то делать и на что-то реагировать. В данном случае это каст — уменьшаем магию и удар — уменьшаем жизнь. Для этого сойдет метод Action(name:String, value:Number):Boolean. Для плагина жизни name == «HIT» и value — забираемая жизнь. Для маны name == «CAST» и value — номер спела, из таблицы спелов будем брать колич отнимаемой маны. Если нельза кастонуть (нет маны или спелла) возвр FALSE, если можно TRUE. Для жизни можно возвращать TRUE если объект еще жив.
Мана регенерируемая, надо еще метод Update(). Для жизни он будет пуст, для маны будет добавлять немного к ее значению если оно меньше максимального запаса.
2.2. Плагин — контейнер. Тоже пространство для фантазии.
Можно сделать фиксированный набор полей для контейнеров (в данном случае уместно их только два) или динамический список с контейнерами.
Есть метод Update — можно в обновлении контейнера тупо дергать все плагины, можно при регистрации плагина указывать в специальном списке обновляемые. Можно было изначально разбить плагины на две группы: обновляемые и нет и разносить их по разным спискам. В уже описанном решении будем тупо дергать Update.
Обычно используются пара стандартных методов при работе плагин-контейнер: а)для регистрации плагина у контейнера и б)удаления его из контейнера. В данном примере я это опущу для краткости.
3) Контейнер. Опять пространство для фантазий.
Плагины списком или полями. Внашем случае сделаем полями — их мало и будет проще. Но это плохо для дальнейшей расширяемости — надо будет переписывать базовые классы, что не хорошо. Также в нашем случае надо будет проверять поле маны на NULL т.е. его может не быть изначально.
Взаимодействие с другими: Если полями, то все просто — другие дергают нужное поле (мана или жизнь) и вызывают универсальный метод. В таком случае можно методы даже сделать не универсальными и плагины не от общего предка.
Независимо от того, полями или списками, можно работать не с плагинами напрямую, а с контейнером. Тогда универсальными методами надо обкладывать его. Т.е. дергаем у контейнера GetProp(name:string):Object он шарит по своим контейнерам (по полям или спискам) и возвращает значение. Если свойства нет — возвращается NULL и по желанию генерится ошибка и/или запись в лог.
В общем и кратко как-то так.
Приведенная система неполна — надо учитывать достижение жизни нулю и смерть, для этого надо методы регистрации плагина в контейнере, чтоб плагин знал кого мочить, когда в нем закончилась жизнь. Это просто для примера. Так-же можно разбивать систему с п.3, а не с п.1.
Надеюсь, понятно в общих чертах.
Для большего запутывания представь что метод Update можно тоже сделать плагином. И будет он цепляться к глобальному списку обновляемых. И конетйнер в котором обновляемый плагин с подплагином обновления будет не знать что у него есть обновляемые плагины :)
Это в принципе понятно, проблемы начнутся когда будет нестандартный плагин, например «движение».
Тогда получится у каждого плугина будет много лишних пустых методов.
И с регистрацией темный момент, как универсально подключать и работать с плагинами. По идее объект не должен знать о деталях работы плагина, а плагин должен сам манипулировать объектом (менять жизнь, положение)
Универсального ничего нет. Все универсально в рамках разработанной системы.
Если постараться можно найти задачу на которой система загнется. Или по невозможности или большой сложности реализации, или по производительности. Потому что более универсальная система обычно — менее производительна.
По поводу пустых методов — Опять же все зависит от архитектуры. Чтоб не дергать все почем зря, уже описал — используется регистрация/удаление.
Также для этого есть «групы плагинов»
Я выше писал, что можно например реализовать обновляемые и необновляемые плагины. Они будут в разных списках контейнера. Будут разные методы у них дергаться.
Опять же плагины могут содержать другие, что выводит систему на следующий уровень гибкости.
В регистрации/удалении ничего темного нет. Темное это в голове. Исходим из того: нужно ли нам знать контейнер (для того чтоб автоматом мочить его по истечении жизни нужно) нужно ли подключаться (регится) в особых списках, событиях (обновление), нужно ли потом от них отключаться.
А темно это потому, что наверняка котсрукторов/деструкторов мало писали. В АС3 так вообще ток конструкторы писать надо. Однако с событиями они подкузьмили — от них надо отписываться — некий аналог деструкторов есть.
ps: для истории, если кто будет это читать. Вот статья с картинкой, наглядно описывающей плагинную архитектуру (дурные мысли сходятся): cowboyprogramming.com/2007/01/05/evolve-your-heirachy/
Сама картинка:
Получается следующее:
1. есть базовый плагин (без методов) — этот шаг можно опустить. Тогда в объекте будет 28 списков для групповых типов.
2. от него наследуются «базовый групповой плагин» (определяет пустой метод или несколько методов. Ну там, имяМетода(строка) или имяМетода(инт, инт))
3. от «базового группового метода» наследуются конкретные плагины, которые переопределяют методы
Я правильно понял?
Если групп плагинов 28 (хотя совневаюсь что столько надо). То надо 28 базовых.
1 общий базовый будет нужен, если есть общие методы у всех 28 групп. Такого не бывает, если специально не придумать.
Смысл в том, что группа плагинов обладает уникальным интерфейсом, так?
Ну там:
1. группа, которая реагирует на мышь — имяМетода(координаты мыши)
2. группа, которая реагирует на клаву — имяМетода(состояние клавиатуры или кнопка)
3. группа, которая срабатывает по таймеру — имяМетода(текущее время)
и т.д.
Как-то так я и понимаю компонентную систему (большие дядьки из GPG тоже).
Осталось только выяснить детали про контейнеры.
См. ветку ниже.
Претензии:
1. Пустые общие методы, так делать нельзя (Update() из примера). Если доводить до абсурда — давай вынесем его в базовый класс и обложим кучей ифов с проверками.
А если у меня какие-то плагины обрабатывают onMouse, onKeyboard, onNetwork?
Для этого тоже пустые методы лепить?
2. Общий метод действия с фиксированным типом параметров (Action()). Ну это совсем неэцсамое. Мне нужны нормальные методы, которые принимают нормальные типы.
У одного плагина 3 метода, у другого 8. Все с разными сигнатурами.
Ладно, тут спорить можно долго, при этом я еще не дописал свою «систему».
Как допишу — сделаю пост, там можно будет всласть покритиковать :)
Все решаемо в рамках плагинной архитектуры.
Большая универсальность — более универсальные методы. Меньшая — менее. И сказал же что все для примера. Из пушки по воробьям. Универсальность сама по себе — плагинная архитектура сама по себе.
Задачу жизнь мана, которая есть или нет совсем решается Классом с тремя полями: жизнь, мана, флаг маны.
Для этого плагины или подписываются сами на нужные сообщения или регятся в нужных списках или контейнера или общих обработчиках ввода, что, впрочем, аналог подписки на сообщения. Ток работает быстрее.
А при отцеплении от контейнера — отписываются, соответственно.
Видимо иду в правильном направлении.
Как я вижу происходящее:
1. В объекте регистрируется компонент (жизнь/движение), при этом он подписывается на события.
2. При касающемся его событии он обновляет свое состояние (если удар — уменьшаем жизнь, импульс — двигаемся).
3. Компоненты реализуют общий интерфейс и имеют общие методы, например update, но при этом реализация у каждого своя и производит совершенно разные действия.
Как-то так, осталось реализовать ;)
В теории многие пишут, что компонентная система превосходит наследование, но практических примеров под флеш не видел.
Про реализацию это ежу понятно, но и интерфейсы тоже разные(!). Иначе скатываемся в ифы.
В один метод мне нужно передать строку, а в другой метод надо передать два инта. И как тут быть?
Если же надо дергать в них разные методы и каждый реализует свой интерфейс, то в чем соль? Это уже другая, менее гибкая, архитектура.
Если надо передавать можно массив с элементами или ...args
Это приводит к ифам, которые плагины/компоненты/наследование пытаются заменить.
Нельзя пнуть ящик, вылечить союзника и крикнуть матом с помощью одного метода.
Опять скатываемся в толстый иф.
Нафига тогда вообще париться? Давайте лепить все в один объект.
Даже на с/с++ пишутся универсальные функции и методы, где передется указатель на структуру. Сам метод объявляется с универсальным указателем void*. Метод приемник, как и передатчик знают какую структуру передавать. Стоит просто типизация в начале метода и все.
На АС3 на Object это делается. А там хоть массивы пихай, хоть произвольный Object, хоть типизированный Object.
Таким методом можно вообще всю логику игры запихать в одну большую функцию, которая принимает (sender, receiver, eventType, args[]).
Дык нет универсальных решений. Или производительность или потери в ущерб универсальности. Чем-то приходится жертвовать.
зы: args[] я предлагаю заменить на универсальный указатель Object или * (как кому нра). Потому как args[] только вариант что можно передать в Object.
А что там кастовать один раз в начале функции приводим и все.
Но опять же там где нужна скорость ни о какой универсальности не может быть и речи.
Например ты на жизнь воздействуешь или на положение? На жизнь — вызываешь у контейнера subLife(n:int), на перемещение — moveVec(x:Number, y:Number). Контейнер в методах обработки дергает нужные или одиночные плагины или прогоняет по их спискам.
У контейнера есть ВСЕ методы, которые переадресуются плагинам?
OMG… 8о
Вот или лепи кучу ифов со стейтмашинами или красиво в одном методе одной строчкой кода учти все баффы, зелья, перки и абилки и многое другое при ударе/касте и прочем.
1. спрашиваем у контейнера ссылку на компонент определенной группы
2. кастуем компонент к групповому типу
3. вызываем групповой метод с конкретными параметрами
Тоже не сильно удобно, но имхо лучше, чем иметь пачку прокси-методов в одном классе.
Чем лучше? Не надо переписывать контейнер при добавлении нового компонента.
Это то-же самое. ТОлько мой вариант быстрее, т.к. меньше вызовов.
1. вызываем групповой метод с конкретными параметрами
2. в том-же методе вызываем нужный плагин/компонент. Все.
Если ты про то, что плагин возможен только 1 на контейнер, так нафик парится вобще. (описывал же кстати) — пишем в поле сразу. Если есть возможно что свойство/плагин или есть или нет, тогда чекаем предварительно:
if(c.field)c.field.doSome(1,2,3);
На АС3 вобще реализуется еще проще с помощью указателей на функции, чтоб не мудрить с компонентами. Делаешь несколько методов-обработчиков нужного поля/полей но дергаешь их через поле-указатель: Function.
Быстродействие:
Быстрее только 1 прямой вызов.
Вызов через указатель в 2.5 раз дольше.
Можно намудрить с интерфейсами 2.1 раза дольше — там один фик два вызова получаются и обращение к полю.
Будут 2 функции в контейнере?
1. МожетБытьИзлечен()
2. Вылечить().
Можно ли пнуть объект? Еще 2 функции.
Можно ли выстрелить в объект? Еще 2 функции.
Контейнер плагинов вообще не должен знать, что именно он содержит.
Он содержит плагины/компоненты. Он умеет доставать объект по идентификатору и отдавать его вызывающему.
А жизнь там или магия, его совершенно не касается.
зы: Причем тут ас3 вообще? Я про идею, а не реализацию.
Или этого не достаточно?
В случае списка — зависит от решения тоже. Можно сделать чтоб при 0 плагинов список пропадал — тогда в поле так-же будет ноль. Можно чекать количество плагинов в списке.
Это от реализации зависит и только.
Я что-то не пойму. Ты вроде от универсальности отказываешься, а пыташься решать задачи универсально.
Для универсального решения можно:
1) сделать метод-запрос для определения есть ли конкретный плагин или группа плагинов.
Но зачем??? Когда можно:
2) Просто вызвать метод, а контейнер или плагины уже сам(и) разберуться могут они его обработать или нет. Если плагин не умеет подтверждать обработку метода, то можно возвращать TRUE если обработка была или FALSE, если ее не было.
Почему? Он это может сделать в любой момент. Списки, поля то в нем. То что он не должен — это ты выдумываешь, а то что он может это знать — реальность.
Ну вот для того как ты хочешь, чтоб контейнер — это чисто массив плагинов. И чтоб его не переписывать, то тут как-бы да он должен просто доставать нужный(-ые) плагин(ы) по запросу. Только быстродействие теряется. Object уже с этим справляется стандартно.
Ну и ООП как-бы для того и придумали чтобы поддержку упростить. Наследуем от нужного потомка или базового контейнера и пишем в наследнике нужные поля, методы, списки. Оно когда такой объект один в игре как-то и не сподручно, но когда их куча — то как-бы и полезно даже.
Летает, плавает, ползает, ходит — объекты могут обладать любым возможным сочетанием этих свойств.
Как ты это все отнаследуешь?
Это риторический вопрос, потому что никак.
Вся эта муть с компонентами — чтобы как раз не наследоваться игровыми объектами. Компоненты наследуются друг от друга («тупым» имплементированием абстрактного метода), «контейнеры» нет.
Игровой объект это «массив плагинов». Он полностью определяется своими свойствами.
Сам о себе он ничего не знает.
В этом весь смысл.
Ладно, давай на этом закончим.
Сделаю — покажу.
Но плагины — только один из вариантов решений.
зы: и я же ясно написал про наследование контейнеров.
Буду изучать ;)
Заинтересовался тут вашей беседой)
Как-то тоже интересовался паттернами, так что оставлю этот плейлист здесь
Очень доходчиво и в сфере геймдева =) По ссылке паттерн стратегии, вроде именно то, что вам нужно
PBE — крут, очень. Только все текущие прожекты уже начаты без него и переписывать естественно лень. Даже если отбросить его главную фишку — component-based модель, то все равно там много достойных вещей — рекомендую покопаться в InputManager, ProcessManager, системе рендеров. Отличная организация + документация, гораздо лучше чем flixel или flashpunk.
Нызывать замену множественного наследование компонентами — это маркетинг чистой воды. До того чтобы это действительно можно назвать компонентами (Дельфя, Оберон) там очень далеко. Но конечно если у них далекоидущие планы, то это поправится, но потом. А пока — рано.
com\pblabs\engine\entity\EntityComponent.as — Типичный базовый плагин.
Соответственно, com\pblabs\engine\entity\Entity.as — типичный контейнер.
addComponent/removeComponent — типичные методы регистрации/удаления плагина.
Ну и интерфейсы их посмотрите.
Кстати, если посмотреть com\\pblabs\\components\\basic\\HealthComponent.as, то там отлично видно, что определяются конкретные функции (public function get isDead():Boolean), которые без динамиккаста никак не вызовешь.
Так например стэйтмашины можно сделать на плагинной архитектуре полностью.
Т.е. в ПБЕ, в принципе, корректнее называть плагины компонентами. Они не юзают именно архитектуру. Но до настоящих компонентов там еще расти, потому имо корректнее на данный момент — обрубок плагинной архитектуры.
Но если названию компонент придумали и утвердили после Делфей другое значение, то поправьте, а то я не в курсе :)
Ну вопчим, описанный тобой подход в плагинной архитектуре используется как правило. Есть методы, которые позволяют определить есть ли данный плагин или расширение (тоже название еще одно) перед тем как его использовать.
Ну и без приведения никак. Или универсальный метод с приведением или конкретный. Но для конкретности лучше юзать чистое ООП тогда.
На ивентах можно ООП реализовать, стейт-машины сами собой получаются плюс это ближе к асинхронному программированию, что несколько сложно, но будет востребовано не только на флеше.
Но нормально чтоб на event-driven писать надо еще перестраиваться. Мне до конца так и не удалось.
Зато по гибкости и возможностям и удобству сопровождения — то что надо.
Опыта еще мало — только этим летом прочитал GoF(Банда четырех). Книга совсем не понравилась, мало понял, примеры на cpp и smalltalk — буэ.
as3dp.com — тоже не нравится. Могу посоветовать — sourcemaking.com/design_patterns
На русском языке читал хорошие статьи по паттернам тут — garbage-collector.ru/, точно помню было про синглтон и прототип.
Сам использую фабрику, сиглтон, state machine и пул объектов. Реализации стандартные(кроме синглтона), они описаны везде, поэтому тут приводить их не буду.
Одно время хотел использовать NullObject. Часто бывает нужно во время обхода массива, что-то удалить из него, а splice() — тормозная собака. Так вот — вместо удаления можно подставлять NullObject, который имеет точно такой же интерфейс, как и реальный объект, только методы никак не реализованы. Если погуглить можно найти описание получше :)
Но забил на это дело.
Вообще банду четырех читал, довольно интересно, но на практике применять не научился, все равно только постфактум осознаю, что вот это был такой-то паттерн.
И последнее, я всегда, если по дизайну синглтон, делаю просто все методы и переменные статическими.
Я может, конечно, чего-то не понимаю, но имхо не вижу необходимости в нем если софтина не многопоточная оО.