Как избежать круговых зависимостей между игроком и миром?

Я работаю над 2D-игрой, где вы можете перемещаться вверх, вниз, влево и вправо. У меня есть по существу два игровых логических объекта:

  • Игрок: Имеет положение относительно мира
  • Мир: Рисует карту и плеер

Пока World зависит от Player (т. е. имеет ссылку на него), нуждаясь в своей позиции, чтобы выяснить, где рисовать персонаж игрока, и какую часть карту для рисования.

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

Самый простой способ, по которому я могу думать, - это Player запросить World , если это возможно. Но это приведет к циклической зависимости между Player и World (т. Е. Каждая содержит ссылку на другую), чего, похоже, стоит избегать. Единственный способ, которым я придумал, - это World переместить Player , но я нахожу его несколько неинтуитивным.

Каков мой лучший вариант? Или избежать круговой зависимости не стоит?

61 голос | спросил futlib 13 22012vEurope/Moscow11bEurope/MoscowTue, 13 Nov 2012 14:35:26 +0400 2012, 14:35:26

9 ответов


61

Мир не должен себя нарисовать; Рендереру следует нарисовать Мир. Игрок не должен рисовать себя; Renderer должен привлечь игрока по отношению к Миру.

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

Я думаю, что мир, вероятно, вообще не должен знать о игроке; это должен быть примитив низкого уровня, а не объект-бог. Игроку, вероятно, потребуется вызвать некоторые методы World, возможно, косвенно (обнаружение конфликтов или проверка для интерактивных объектов и т. Д.).

ответил Liosan 13 22012vEurope/Moscow11bEurope/MoscowTue, 13 Nov 2012 15:18:10 +0400 2012, 15:18:10
36

Вот как типичный механизм рендеринга обрабатывает эти вещи:

Существует фундаментальное различие между тем, где объект находится в пространстве и как рисован объект.

  1. Рисование объекта

    Обычно у вас есть класс Renderer , который делает это. Он просто берет объект (Модель) и рисует на экране. Он может иметь такие методы, как drawSprite (Sprite), drawLine (..), drawModel (Model), что бы вы ни захотели. Это рендерер, поэтому он должен делать все это. Он также использует любой API, который у вас есть, поэтому вы можете иметь, например, рендеринг, который использует OpenGL, и тот, который использует DirectX. Если вы хотите портировать игру на другую платформу, вы просто пишете новый рендерер и используете его. Это «легко».

  2. Перемещение объекта

    Каждый объект привязан к тому, что мы хотели бы назвать SceneNode . Вы достигаете этого через композицию. Объект SceneNode содержит объект. Вот и все. Что такое SceneNode? Это простой класс, содержащий все преобразования (положение, поворот, масштаб) объекта (обычно относительно другого SceneNode) вместе с фактическим объектом.

  3. Управление объектами

    Как управляются SceneNodes? Через SceneManager . Этот класс создает и отслеживает каждый SceneNode в вашей сцене. Вы можете запросить его для определенного SceneNode (обычно обозначенного строковым именем, таким как «Player» или «Table») или список всех узлов.

  4. Рисование мира

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

  5. Обнаружение столкновений

    Это не всегда тривиально. Обычно вы можете запросить сцену о том, какой объект находится в определенной точке пространства, или о том, какие объекты пересекут луч. Таким образом вы можете создать луч от вашего игрока в направлении движения и спросить менеджера сцены, что является первым объектом, который пересекает луч. Затем вы можете переместить игрока на новую позицию, переместить его на меньшее количество (чтобы получить его рядом с сталкивающимся объектом) или вообще не перемещать его. Убедитесь, что эти запросы обрабатываются отдельными классами. Они должны спросить SceneManager о списке SceneNodes, но еще одна задача - определить, охватывает ли этот SceneNode точку в пространстве или пересекает ее с лучом. Помните, что SceneManager создает и сохраняет узлы.

Итак, что такое игрок, и что такое мир?

Игрок может быть классом, содержащим SceneNode, который, в свою очередь, содержит визуализированную модель. Вы перемещаете плеер, изменяя положение узла сцены. Мир - это просто экземпляр SceneManager. Он содержит все объекты (через SceneNodes). Вы обрабатываете обнаружение столкновений, делая запросы на текущее состояние сцены.

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

ответил rootlocus 13 22012vEurope/Moscow11bEurope/MoscowTue, 13 Nov 2012 20:48:39 +0400 2012, 20:48:39
16

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

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

<сильный> Edit
Хорошо, чтобы ответить на вопрос: вы можете избежать того, что Игроку нужно знать World для проверки на столкновение с помощью обратных вызовов:

  World :: checkForCollisions ()
{
  [...]
  foreach (entityA в entityList)
    foreach (entityB в entityList)
      if ([... entityA и entityB столкнулись ...])
         entityA.onCollision (entityB);
}

Игрок :: onCollision (другое)
{
  [... реагировать на столкновение ...]
}
 

Вид физики, который вы описали в вопросе, может обрабатываться миром, если вы обнаружите скорость сущностей:

  World :: calculatePhysics ()
{
  foreach (entityA в entityList)
    foreach (entityB в entityList)
    {
      [... переместить объект А в соответствии с его скоростью, насколько это возможно ...]
      если ([... entityA столкнулся с миром ...])
         entityA.onWorldCollision ();
      [... вычислить движение сущности B, чтобы знать, столкнулся ли A с B ...]
      if ([... entityA и entityB столкнулись ...])
         entityA.onCollision (entityB);
    }
}
 

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

ответил API-Beast 13 22012vEurope/Moscow11bEurope/MoscowTue, 13 Nov 2012 16:37:50 +0400 2012, 16:37:50
13

Ваш текущий дизайн, похоже, противоречит первому принципу ТВЕРДЫЙ дизайн .

Этот первый принцип, называемый принципом «единой ответственности», как правило, является хорошим ориентиром для того, чтобы не создавать монолитные объекты, которые всегда будут наносить ущерб вашему дизайну.

Чтобы конкретизировать, ваш объект World отвечает как за обновление и сохранение состояния игры, так и за рисование всего.

Что делать, если ваш код рендеринга изменяется /должен меняться? Почему вам нужно обновлять оба класса, которые на самом деле не имеют никакого отношения к рендерингу? Как уже сказал Liosan, у вас должен быть Renderer .


Теперь, чтобы ответить на ваш реальный вопрос ...

Есть много способов сделать это, и это только один способ развязки:

  1. Мир не знает, что такое игрок.
    • У него есть список Object s, в котором находится игрок, но он не зависит от класса игрока (используйте для этого наследование).
  2. Игрок обновляется некоторым InputManager .
  3. Мир обрабатывает обнаружение движения и столкновения, применяя надлежащие физические изменения и отправляя обновления объектам.
    • Например, если объект A и объект B сталкиваются, мир проинформирует их, а затем они смогут обработать его самостоятельно.
    • Мир по-прежнему будет заниматься физикой (если ваш дизайн такой).
    • Затем оба объекта могли видеть, интересует ли их конфликт или нет. Например, если объект A был игроком, а объект B был шипом, тогда игрок мог нанести ему ущерб.
    • Это можно решить другими способами.
  4. Renderer рисует все объекты.
ответил jcora 13 22012vEurope/Moscow11bEurope/MoscowTue, 13 Nov 2012 18:02:06 +0400 2012, 18:02:06
1

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

То, что вы хотите избежать в круговых ссылках, не столько поддерживает ссылки друг на друга, но скорее ссылается друг на друга явно в коде.

ответил Tom Johnson 13 22012vEurope/Moscow11bEurope/MoscowTue, 13 Nov 2012 14:54:11 +0400 2012, 14:54:11
1

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

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

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

ответил Calmarius 13 22012vEurope/Moscow11bEurope/MoscowTue, 13 Nov 2012 21:43:40 +0400 2012, 21:43:40
0

Извлекая детали о игроке и мире, у вас есть простой случай, когда вы не хотите вводить круговую зависимость между двумя объектами (что в зависимости от вашего языка может даже не иметь значения, см. ссылку в комментарии Fuhrmanator). Есть как минимум два очень простых структурных решения, которые применимы к этой и аналогичным проблемам:

1) Внесите singleton в свой мир . Это позволит игроку (и любому другому объекту) легко находить объект мира без дорогостоящих поисков или постоянно удерживаемых ссылок. Суть этого шаблона заключается в том, что класс имеет статическую ссылку на единственный экземпляр этого класса, который устанавливается на экземпляр объекта и очищается от его удаления.

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

2) Если язык, который вы разрабатываете, поддерживает его (многие делают), используйте Слабая ссылка . Это ссылка, которая не влияет на такие вещи, как сбор мусора. Это полезно в точно таких случаях, просто не забудьте сделать какие-либо предположения о том, существует ли еще слабый ссылочный объект.

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

ответил FlintZA 16 52012vEurope/Moscow11bEurope/MoscowFri, 16 Nov 2012 17:18:12 +0400 2012, 17:18:12
0

Как говорили другие, я думаю, что ваш World делает одну вещь слишком много: она пытается обе содержать игру Map (который должен быть отдельным объектом) и одновременно Renderer .

Итак, создайте объект new (возможно, GameMap ) и сохраните в нем данные уровня карты. Напишите в нем функции, которые взаимодействуют с текущей картой.

Затем вам также нужен объект Renderer . Вы могли сделать этот объект Renderer тем, что оба содержат GameMap и Player ( а также Enemies ), а также рисует их.

ответил bobobobo 8 thEurope/Moscowp30Europe/Moscow09bEurope/MoscowSun, 08 Sep 2013 04:13:06 +0400 2013, 04:13:06
-6

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

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

ответил snake5 13 22012vEurope/Moscow11bEurope/MoscowTue, 13 Nov 2012 14:59:39 +0400 2012, 14:59:39

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132