Как обращаться с иерархией классов - PullRequest
3 голосов
/ 02 августа 2011

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

У меня есть базовый класс BaseCharacter, который определяет персонажа в моей игре. Он содержит все основные характеристики, держатели эффектов статуса, события и т. Д., Чтобы иметь базовый характер. Большая часть моей игровой логики работает с сущностями BaseCharacter, разыгрывая при необходимости.

Оттуда я наследую класс гуманоидов, который добавляет необходимые структуры и методы для добавления экипировки (оружие, броня и т. Д.) К персонажу, затем, наконец, у меня есть класс PlayableCharacter, который наследуется от гуманоида и добавляет такие вещи, как текущий опыт. настраиваемые структуры способностей и т. д.

Итак, структура класса просто:

BaseCharacter ---> Гуманоид ---> PlayableCharacter

Эта структура отлично подходит для определения персонажей на стороне игрока, хотя я могу решить немного улучшить структуру. Я должен иметь возможность добавлять "вражеские" классы в иерархию. Чтобы удовлетворить требования к противнику, мне нужно добавить некоторые структуры данных / методы, связанные с EXP / точками для победы над врагом, таблицами добычи и т. Д.

Проблема в том, куда мне добавить эти структуры? Добавление их в BaseCharacter кажется плохой идеей, поскольку я в основном говорю, что у каждого персонажа есть необходимые методы / структуры данных для вражеской сущности. Я подумал о том, чтобы запустить отдельный поток классов, который в итоге выглядел бы так:

BaseCharacter--->Humanoid--->PlayableCharacter
           \---->Enemy--->EnemyHumanoid

где враг наследует от BaseCharacter. EnemyHumanoid добавит тот же набор методов / структур данных, что и наследование от BaseCharacter-> Humanoid. Это работает, но означает, что я не могу разыграть EnemyHumanoid в Humanoid, несмотря на то, что EnemyHumanoid удовлетворяет требованиям Humanoid. Это также добавляет избыточность, так как я хотел бы, чтобы эти два наследования добавляли один и тот же набор объектов / методов.

затем рассматривается использование отдельного класса EnemyInterface, и в итоге получается иерархия наследования, подобная этой:

BaseCharacter----->Humanoid----->PlayableCharacter
    |                 |                
    V                 V                
  Enemy         EnemyHumanoid   

Enemy и EnemyHumanoid все реализуют EnemyInterface. Однако теперь я теряю отношения Enemy <-> EnemyHumanoid. Должен ли я использовать множественное неравномерность здесь? (есть ли в EnemyHumanoid от Enemy and Humanoid?) это считается плохой формой? Есть ли более логичный способ сделать это? Пример, приведенный здесь, довольно прост, но если я в итоге разделю иерархию классов еще больше, это станет еще сложнее.

Буду признателен за любую помощь

Ответы [ 6 ]

2 голосов
/ 02 августа 2011

Вместо того, чтобы перестраивать иерархию классов для решения этой конкретной проблемы (см. Ответ StackUnderblow, чтобы узнать, как это сделать), я бы посоветовал вам провести некоторое исследование архитектуры на основе компонентов.По сути, вместо того, чтобы иметь класс, описывающий вещи, которые делают PlayableCharacter уникальным, у вас будет компонент (или набор компонентов), привитой к какому-либо базовому контейнеру.

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

Здесь - это хорошее SO-обсуждение, расширяющее мое предложение.

Бхишмар прав, ты на пути в ад!

1 голос
/ 02 августа 2011

Вы пытаетесь подклассифицировать свой класс BaseCharacter в соответствии с несколькими аспектами (ошибаюсь, я не уверен, что «аспект» - правильное слово здесь, я не являюсь носителем английского языка, извините), а именно:

  • Форма - Гуманоид / Животное / и т.д.
  • Выравнивание - Враг / Друг / и т. Д.

Это всегда приводит к проблемам, как вы уже заметили: рано или поздно вы захотите создать классы, которые должны наследоваться от нескольких подклассов BaseCharacter (обычно по одному от каждой категории аспектов), например. Гуманоид + Враг. В этом случае вы либо используете множественное наследование (не рекомендуется в C ++ из-за проблем с реализациями), либо вам нужно переделать свой код.

Обычно лучшим решением является разделение вашего объекта в соответствии с аспектами, определенными ранее. Например. внутри объекта BaseCharacter вы можете иметь два указателя: CharacterForm * и CharacterAlignment *. Затем вы можете создавать подклассы CharacterForm (как HumanoidForm, AnimalForm и т. Д.) И CharacterAlignment (EnemyAlignment, FriendAlignment и т. Д.) Независимо и комбинировать их по мере необходимости.

Обратите внимание, что этот подход действительно является первым шагом к проектированию на основе компонентов, как предложил Дэн О.

Вам нужно делать что-то подобное каждый раз, когда вы создаете подкласс, который является подклассом его базы в другом смысле, чем предыдущие подклассы (например, после создания Humanoid и Animal вам нужен Enemy -> это явно другой способ классификации ваших BaseCharacters). Практическое правило гласит, что каждый класс должен быть только подклассом в соответствии с одним аспектом.

1 голос
/ 02 августа 2011

Как насчет этого возможного?

  BaseCharacter----->Humanoid----->PlayableCharacter
                      / \                
                     /   \                
                Enemy     EnemyHumanoid   

EnemyHumanoid может потребоваться только некоторые функции от Humanoid.

1 голос
/ 02 августа 2011

Похоже, что вы используете наследование для повторного использования вместо того, чтобы придерживаться LSP (http://en.wikipedia.org/wiki/Liskov_substitution_principle),, который является путем в ад.

Мой общий совет - использовать наследование только для отношений is-a. То есть, где вы можете заменить один тип другим. Я предпочитаю композицию наследованию для большинства решений, поскольку я считаю, что она проще.

В каком конкретном случае вы считаете, что Enemy отличается от PlayableCharacter? Подумайте о том, чтобы иметь таблицы опыта и лута вне иерархии классов, где вы можете передать ему гуманоида и рассчитать эти предметы отдельно.

C ++ - это мультипарадигмальный язык. Вам не нужно ограничиваться ОО-решениями.

Удачи!

0 голосов
/ 02 августа 2011

Как насчет этого?

BaseCharacter

IBehaviour - это интерфейс

HumanoidBehaviour - это реализация IBehaviour

BaseCharacter содержит IBehaviour

Enemy наследует BaseCharacter и содержит HumanoidBehaviour

PlayableCharacter наследует BaseCharacter и содержит HumanoidBehaviour

Так что логически мыВы получите: Enemy is-a BaseCharacter и has-a поведение HumanoidBehaviour.

Возможно Шаблон разработки стратегии здесь будет полезно. Также есть забавная книга с хорошим примером)

0 голосов
/ 02 августа 2011

Необходимо рассмотреть вопрос о том, нужно ли преобразовывать экземпляр Humanoid в экземпляр BaseCharacter.Вы можете обнаружить, что на самом деле это не так.Вам действительно нужно перебирать набор всех персонажей-гуманоидов?

Если так, определите:

Humanoid

BaseCharacter->PlayableCharacter (contains Humanoid)
            \---->Enemy---->EnemyHumanoid (contains Humanoid)

[Возможно, вы захотите сделать абстрактный враг определяющим конкретный EnemyNonhumanoid.]

Альтернативой является определение Humanoid как интерфейса и предоставление метода getHumanoid () для PlayableCharacter и EnemyHumanoid, реализованного в качестве дескриптора в каждом случае.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...