Проблема "раздутого" класса
Друзья, поделитесь опытом.
У меня по мере разработки игры слишком разросся класс Character. Причём если смотреть на него с т.з. ООП, то вроде всё на месте. Есть штук 25 постоянных и неизменных на протяжении всей игры свойств, ещё штук 20 изменяемых параметров, описывающих текущее состояние, полтора десятка умений, что-то ещё по мелочам. Вся ботва с сеттерами/геттерами - это уже около 700 строк кода. Дальше идут методы. Их много, т.к. много чего должно рассчитываться для персонажа, в том числе по-своему в зависимости от наследника Character. Так что особо не уберёшь никуда. Таким образом, даже реализовав менеджер экипировки и менеджер статус-эффектов как отдельные классы, всё равно кол-во строк зашкалило за 1500. Работать с такой громадиной становится затруднительно. Как решаются подобные ситуации? Единственное, что пока приходит в голову, это сделать отдельный класс CharacterData для неизменных "справочных" значений и передавать в конструктор каждого экземпляра Character. Но всё равно будет геморрой с созданием наследников CharacterData, плюс в самом Character не обойтись без записей записей типа: Код AS3:
|
Если я верно понимаю проблему, то она решается цепочкой наследования, не обязательно чтобы у каких-то сущностей было несколько потомков, сущность может выступать в роли родителя просто ради того чтобы упростить структуру детей. Так напрмер когда-то я делал сущность определяющую взаимоотношения Видео и Рекламы. В зависимости от типа рекламы и момента воспроизведения видео таких взаимоотношений получалось доасточно много, я решил эту проблему сделав цепочку наследования типа Base->WithPreroll->WithMidroll->WithPostroll... При поддержке подобного кода и обнаружении проблем в прероллах или мидроллах в первую очередь смотрел что не так в специфичных сущностях. Думаю у вас тоже можно выделить какие-то более базовые логичные сущности определяющие конкретные аспекты вашего класса.
|
Навряд ли тут вообще есть какие-то линейные решения. Если пойти линейно, то всякое живое существо должно наследоваться от сундука, так ведь? Ибо у любого существа, которое может быть убито, есть какой-то лут: мясо для готовки, ингридиенты для алхимии, компоненты для крафтинга (шкуры, рога, клыки, перья, чешуя и тп), а у человекоподобных вообще любые предметы (в Скайриме добавили "правдоподобности", и в хищниках можно найти мелкие неперевариваемые предметы — монетки, кольца, драгоценные камни))) Даже в самой "минимально навороченной" игре должна быть возможность что-то взять с трупа хотя-бы босса, оружие или квестовый предмет. Но при этом сам набор лута генерится только после смерти рандомно (в пределах возможного для данного вида предметов). То есть лут есть у Трупа, а у живого есть Поведение, которого нет у трупа. Должны ли это быть разные классы? ;)
Нужна ли неписям система слотов для экипировки? Нужно ли нам разбираться (на уровне Модели, не Вью), что вот это надето на ноги, а это — на голову? Или достаточно списка вещей? Если неписи могут иметь дополнительное, то есть НЕ экипированное оружие, доспехи и амулеты, то нам нужно будет знать, какие предметы были экипированы и значит создавали активные эффекты, баффы и резисты. Кроме того, экипированные предметы не могут генериться после смерти непися. А дополнительные могут. Достаточно ли иметь список экипированного — для отображения персонажа и расчета эффектов, а после смерти генерить дополнительный список скрытого лута, и не иметь при этом менеджер слотов как у Героя, которого мы постоянно переодеваем и меняем оружие "вручную"? Ну, в Скае неписи иногда меняют оружие во время боя, лучники могут вытащить топор, а маги кинжал, если мана кончилась) А еще оружие можно выбить из рук. Могут ли животные и монстры иметь эффекты и лечиться во время боя? А если это какой-нибудь волк-компаньон мага, или боевой конь — может маг на него накладывать резисты или баффы и лечить во время боя? От таких нюансов может зависеть система наследования и архитектуры классов вообще. Всегда можно попробовать вынести что-то из Персонажа в Досье о персонаже, то есть не хранить данные О персонаже непосредственно в нем самом, а брать из справочника по айди. Ежу понятно, что вся игра крутится вокруг Героя и все так или иначе замыкается на него. Но должно ли это "всё" именно храниться непосредственно в свойствах экземпляра Героя, или можно хранить это в "досье"? Название локации, координаты героя на локации, список квестов и их активные стадии, прогресс в умениях и прочие статистики — все ли нужно пихать в один объект? Ты и сам уже заметил, что вслед за данными подтягивается и их обработка: не успеешь глазом моргнуть, как экземпляр персонажа начнет у тебя проверять расстояние до стен в коридоре и траекторию полета стрелы, шанс попадания и время суток. Как только появляется нужда в обработке, следом приходит нужда в дополнительных данных из внешнего мира, и ты начинаешь ломать голову, как Герою запросить погоду или количество детей вот этого торговца... Данные должны (в идеале конечно) храниться там, где они обрабатываются. Если программа из совершенно разных мест постоянно лезет в один и тот же объект за данными, которые нужны ей только в ЭТОМ месте, значит что-то пошло не так. Пример: Менеджер квестов не должен дергать экземпляр Героя, чтобы узнать активную фазу текущего квеста. Он должен знать это САМ. Герой — один. Менеджер движения/локации не должен спрашивать у Героя его координаты — он должен хранить их САМ, они больше никому не нужны. Герой не должен хранить в себе специфические данные "для кого-то одного". Можно хранить данные, которые нужны многим и не должны быть рассинхронизированы так, что один менеджер считает что здоровье 100, а другой насчитал 15. Тогда есть смысл. |
Цитата:
Добавлено через 52 минуты Цитата:
Я пока выбрал для себя такой путь. Выделять в отдельные сущности какие-то тематически целостные вещи. Например, ты мне в своё время очень удачно посоветовал реализовать менеджер экипировки как отдельный класс, иначе класс Character ещё сильнее бы увеличился. Сейчас попробовал сделать SkillManager и загнать туда все скиллы, а также методы, связанные с моделью их увеличения. |
Цитата:
Код AS3:
Почитай какие-нибудь статьи на тему плюсов и минусов композиции/наследования. Посмотри на Ash. Это довольно простой фреймворк, реализующий шаблон проектирования Entity-Component-System. Он основывается на использовании композиции вместо наследования. Чаще всего применяется именно для создания игр. Изучи принцип, по которому работает данный фреймворк и наверняка ты пересмотришь свои взгляды на многие вещи, касающиеся организации своего кода. |
Цитата:
|
Appleman, Ну смотри. У тебя есть Character, который использует CharacterData. Да, это композиция, но...
В текущей реализации ты дублируешь все поля CharacterData в Character. Это значит, что изменяя CharacterData, тебе обязательно придется лезть в Character, повторяя все изменения также и в нем. Нарушается принцип черного ящика. Ты неизбежно будешь получать ошибки и добавишь себе кучу рутинной работы. Ты, конечно, можешь поручить рутину своей среде разработки, если она это умеет, используя интерфейсы или функцию делегирования методов, как, например в IntelliJ IDEA. Но даже это полностью не избавит тебя от рутины. Чтобы избавить себя от рутины и кучи потенциальных ошибок, ты можешь сделать Character наследником CharacterData. Все изменения во втором автоматически будут менять и первый. Но тогда теряются все достоинства композиции. Еще ты можешь сделать так, как я писал выше - добавить в Character всего один метод getData():CharacterData. Тогда, меняя CharacterData, ты не пустишь волну изменений в других классах, как в первом случае. Character знает о том, что у него есть характеристики, но о том, какие они, сколько их и как они работают - об этом ему ничего не известно. Ну и четвертый вариант. Ты можешь реализовать все по шаблону ECS, как в Ash. Тогда у тебя вообще исчезнут такие классы, как Character, CharacterNPC, CharacterPlayer и т.д. в том виде, какие они сейчас. У тебя будут сущности, как контейнеры для всевозможных компонентов и будут узлы, которые определяют связки конкретных компонентов. Например, если сущность имеет такие компоненты, как CharacterData, Equipment, Inventory, Player, View, Position, то движок работает с ней, как с классом героя. Заменяем компонент Player на NPC, и это уже бот. Берем уже готовые компоненты Inventory, View, Position и добавляем ItemContainer, и эта сущность становится сундуком. Далее происходит магия. Все эти сущности имеют компоненты View и Position. Этих данных достаточно для того, чтобы поместить объект на игровую карту. Т.е. система, отвечающая за отображение объектов на карте может работать со всеми ими одинаково. И ей плевать, сундук это, дерево или NPC. Данные полностью отделяются от логики, зависимости между компонентами находятся не в самих компонентах, а выносятся на уровень выше. Раздутых классов нет, все лежит на своих местах и занимается одной определенной задачей. В общем это все долго можно расписывать, лучше погугли сам. Даже на этом сайте где-то были уроки по данному подходу. |
что-то я не вижу связи - каким образом ECS подход отменяет Наследование?) CharacterNPC и CharacterPlayer все равно будут наследоваться от Character - либо будет копипаста кода
|
ZergMaster, а я и не утверждал, что ECS отменяет наследование. Хотя, в данном конкретном случае можно организовать все так:
класс Character - это узел; классы CharacterNPC и CharacterPlayer его наследники, но их различия всего в одной строке (ссылке на частные компоненты); классы NPC и Player - компоненты, не имеющие общих корней и описывающие все те данные, что присущи лишь одной из сущностей. Код AS3:
Добавлено через 56 минут Вообще, я сейчас прочитал все то, что написал и понял, что не совсем верно изложил свои мысли. Вся эта писанина относится не к выбору между наследованием и композицией(ECS), а именно к затронутой автором проблеме раздутого класса и способах её решения. Тут были предложены варианты решения через цепочку наследования. Я же предложил решение через композицию. Но, естественно, говоря "композиция", я имею в виду "композиция и наследование". Как мне кажется, если решать "проблему раздутого класса" чисто с помощью цепочки наследования, то мы получим некое разросшееся дерево, с высокой глубиной и при этом один корень будет иметь 2-3 класса. А если решать через ECS, то у нас будет несколько маленьких деревьев с одним корнем и одним поколением наследников. Разросшееся дерево в этом случае может получиться только у цепочки узлов, но это настолько маленькие классы, что запутаться в таком дереве будет очень сложно. |
RedHead90, спасибо за объяснения и примеры.
Цитата:
Я сам изначально вводил приватные переменные в классы-наследники (т.е. делал всё то же, но не в форме отдельного класса, а просто свойствами в имеющийся), а потом надоело мучиться и ловить исключения при обращении за ними в суперкласс Character. В итоге объявил всё в супере, а дальше уже разруливаю по ситуации переопределяемыми в наследниках сеттерами/геттерами. |
Часовой пояс GMT +4, время: 17:52. |
Copyright © 1999-2008 Flasher.ru. All rights reserved.
Работает на vBulletin®. Copyright ©2000 - 2024, Jelsoft Enterprises Ltd. Перевод: zCarot
Администрация сайта не несёт ответственности за любую предоставленную посетителями информацию. Подробнее см. Правила.