Как реструктурировать эту иерархию кода (относящуюся к Закону Деметры)

У меня есть игровой движок, в котором я отделяю физическое моделирование от функциональности игрового объекта. Итак, у меня есть чистый виртуальный класс для физического тела

class Body

из которого я буду выводить различные реализации физического моделирования. Тогда мой класс игрового объекта выглядит как

class GameObject {
public:
   // ...
private:
   Body *m_pBody;
};

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

Vector GameObject::GetPosition() const { return m_pBody->GetPosition(); }

У меня есть искушение поцарапать их все и просто сделать что-то вроде

pObject->GetBody()->GetPosition();

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

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
0
606
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Один из подходов, который вы можете использовать, - это разделить интерфейс Body на несколько интерфейсов, каждый из которых имеет разные цели, и предоставить GameObject право собственности только на те интерфейсы, которые он должен предоставлять.

class Positionable;
class Movable;
class Collidable;
//etc.

Конкретные реализации Body, вероятно, будут реализовывать все интерфейсы, но GameObject, которому нужно только раскрыть свою позицию, будет ссылаться (через внедрение зависимостей) только на интерфейс Positionable:

class BodyA : public Positionable, Movable, Collidable {
    // ...
};

class GameObjectA {
private:
    Positionable *m_p;
public:
    GameObjectA(Positionable *p) { m_p = p; }
    Positionable *getPosition() { return m_p; }
};

BodyA bodyA;
GameObjectA objA(&bodyA);

objA->getPosition()->getX();

Похоже, это просто переносит проблему на Positionable - нам по-прежнему приходится использовать двойную цепочку для получения любой релевантной информации.

Jesse Beder 22.01.2009 18:56

Насколько я понимаю, идея состоит в том, чтобы полностью избавиться от m_pBody. Вместо этого у вас есть интерфейс Positionable, предоставляющий GetPosition () и связанные с ним методы. Затем вы реализуете эти методы в GameBody. Суть Закона Деметры - убедиться, что вещи, которые следует спрятать, есть.

Max Lybbert 23.01.2009 04:55

Но в чем разница между m_pBody и m_p? (На самом деле, большинство вещей, которые я хотел бы сделать для / чтения из Body, я бы также хотел сделать то же самое для GameObject. Вся эта вещь Positionable / Movable / Collidable, кажется, просто обходит проблему.

Jesse Beder 23.01.2009 08:35

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

У вас могут быть иерархии, такие как корабль-> tie_fighter, корабль-> x_wing. Но не PlaysSound-> tie_fighter. Ваш класс tie_fighter должен состоять из объектов, которые он должен представлять. Часть физики, часть графики и т. д. Вы должны предоставить минимальный интерфейс для взаимодействия с вашими игровыми объектами. Реализуйте как можно больше физической логики в движке или в физической части.

При таком подходе ваши игровые объекты становятся коллекциями более основных игровых компонентов.

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

Недавно я попытался сделать функции состояния более высокого уровня, используя идеи из Box2D. Есть функция SetXForm для установки позиций и т. д. Еще одна для SetDXForm для скоростей и угловой скорости. Эти функции принимают прокси-объекты в качестве параметров, которые представляют различные части физического состояния. Используя подобные методы, вы можете уменьшить количество методов, которые вам понадобятся для установки состояния, но в конечном итоге вы, вероятно, все равно будете реализовывать более мелкие, и прокси-объекты будут больше работать, чем вы бы сэкономили, пропустив по нескольким методам.

Так что я не особо помог. Это было скорее опровержением предыдущего ответа.

Подводя итог, я бы рекомендовал вам придерживаться подхода, основанного на множестве методов. Между игровыми объектами и физическими объектами не всегда может быть простое отношение один к одному. Мы столкнулись с тем, что было намного проще иметь один игровой объект, представляющий все частицы от взрыва. Если бы мы уступили и просто выставили указатель тела, мы не смогли бы упростить задачу.

Я могу просто оставить все повторяющиеся методы. Но это довольно неприятно.

Jesse Beder 22.01.2009 21:59

Правильно ли я понимаю, что вы отделяете физику чего-то от его игрового представления?

то есть, вы бы увидели что-то вроде этого:

class CompanionCube
{
    private:
        Body* m_pPhysicsBody;
};

?

Если так, мне это плохо пахнет. Технически ваш GameObject является является физическим объектом, поэтому он должен быть производным от Body.

Похоже, вы планируете поменять местами физические модели, и именно поэтому вы пытаетесь сделать это с помощью агрегации, и если это так, я бы спросил: «Планируете ли вы менять типы физики во время выполнения или во время компиляции? ? ".

Если время компиляции - ваш ответ, я бы получил ваши игровые объекты из Body и сделал бы Body как typedef для любого физического тела, которое вы хотите использовать по умолчанию.

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

В качестве альтернативы вы, вероятно, обнаружите, что у вас будут разные «родительские» классы для Body в зависимости от типа игрового объекта (вода, твердое тело и т. д.), Поэтому вы можете просто сделать это явным в своем производном.

В любом случае, я перестану болтать, так как этот ответ основан на многих предположениях. ;) Дайте знать, если я не на базе, и я удалю свой ответ.

Это своего рода микс. Я не могу получить игровые объекты из Body, потому что некоторые объекты могут использовать разные типы Body (как вы предложили); но это не так жестко, как «Вода должна происходить от WaterBody», поскольку нам может понадобиться WaterBody1 и WaterBody2, и мы можем выбирать между ними либо во время выполнения, либо во время компиляции.

Jesse Beder 22.01.2009 21:56

Кроме того, проблема с написанием класса Body, который переключается внутри, заключается в том, что он подталкивает ту же проблему к выполнение Body (с тех пор у нас будут аналогично производные классы, скажем, от BodyImpl, и мы захотим переслать все BodyImpl выполняет функции Body).

Jesse Beder 22.01.2009 21:58
Ответ принят как подходящий

Идея закона Деметры заключается в том, что ваш GameObject не должен иметь таких функций, как GetPosition(). Вместо этого он должен иметь функции MoveForward(int) или TurnLeft(), которые могут вызывать GetPosition() (вместе с другими функциями) изнутри. По сути, они переводят один интерфейс в другой.

Если ваша логика требует функции GetPosition(), имеет смысл превратить ее в интерфейс a la Ates Goral. В противном случае вам придется переосмыслить, почему вы так глубоко захватываете объект, чтобы вызвать методы его подобъектов.

GameObject определенно нуждается в таких вещах, как GetPosition (), поскольку это может быть, скажем, мяч (для которого MoveForward не имеет смысла).

Jesse Beder 22.01.2009 18:57

В этом случае «ваша логика требует функции GetPosition ()», и поэтому «имеет смысл превратить ее в интерфейс a la Ates Goral». Но вам следует «переосмыслить, почему вы захватываете ... объект» (т.е. определить, нужно ли вам изменить дизайн, скажем, с помощью ImmovableGameObjects и MovableGameObjects).

Max Lybbert 22.01.2009 23:07

Как будет работать этот редизайн? Также см. Мой комментарий к Атесу Горалу.

Jesse Beder 22.01.2009 23:56

Я передумал - на самом деле я к этому и собираюсь. Что меня раньше смущало, так это то, что MoveForward и TurnLeft не имеют смысла в моем контексте, но другие функции (с аналогичной идеей) имеют.

Jesse Beder 23.01.2009 08:37

Другие вопросы по теме