Я работаю над 2D-игрой, где вы можете перемещаться вверх, вниз, влево и вправо. У меня есть два игровых логических объекта:
- Игрок: имеет позицию относительно мира
- Мир: рисует карту и игрока
Пока что Мир зависит от Игрока (т.е. имеет ссылку на него), и ему необходимо определить, где нарисовать персонажа игрока и какую часть карты нарисовать.
Теперь я хочу добавить обнаружение столкновений, чтобы игрок не мог двигаться сквозь стены.
Самый простой способ, который я могу придумать, - попросить игрока спросить мир, возможно ли предполагаемое движение. Но это привело бы к круговой зависимости между игроком и миром (т. Е. Каждая содержит ссылку на другую), которую, похоже, стоит избегать. Единственный способ, которым я придумал, - заставить Мир перемещать Игрока , но я нахожу это несколько не интуитивным.
Какой мой лучший вариант? Или избегать круговой зависимости не стоит?
Ответы:
Мир не должен рисовать сам; Рендерер должен нарисовать мир. Игрок не должен рисовать сам; Рендерер должен нарисовать Игрока относительно Мира.
Игрок должен спросить Мир об обнаружении столкновений; или, возможно, столкновения должны обрабатываться отдельным классом, который проверял бы обнаружение столкновений не только против статического мира, но и против других действующих лиц.
Я думаю, что Мир, вероятно, вообще не должен знать об Игроке; это должен быть примитив низкого уровня, а не божественный объект. Игроку, вероятно, потребуется вызывать некоторые методы World, возможно, косвенно (обнаружение столкновений или проверка интерактивных объектов и т. Д.).
источник
Renderer
некоторый тип a , но это не означает, что логика того, как каждая вещь отображается, обрабатываетсяRenderer
, каждая вещь, которая должна быть нарисована, вероятно, должна наследоваться от общего интерфейса, такого какIDrawable
илиIRenderable
(или эквивалентный интерфейс на любом языке, который вы используете).Renderer
Полагаю, мир мог бы быть таким , но кажется, что он переступил бы свою ответственность, особенно если бы он уже был собойIRenderable
.Вот как типичный движок рендеринга обрабатывает эти вещи:
Существует фундаментальное различие между тем, где объект находится в пространстве и как объект рисуется.
Рисование объекта
Обычно у вас есть класс Renderer, который делает это. Он просто берет объект (модель) и рисует на экране. Он может иметь такие методы, как drawSprite (Sprite), drawLine (..), drawModel (Model), все, что вам нужно. Это рендерер, поэтому он должен делать все эти вещи. Он также использует любой API, который у вас есть, так что вы можете иметь, например, средство визуализации, которое использует OpenGL, и то, которое использует DirectX. Если вы хотите перенести свою игру на другую платформу, вы просто пишете новый рендерер и используете его. Это "так" легко.
Перемещение объекта
Каждый объект прикреплен к чему-то, что мы хотели бы назвать SceneNode . Вы достигаете этого через композицию. SceneNode содержит объект. Вот и все. Что такое SceneNode? Это простой класс, связывающий все преобразования (положение, вращение, масштаб) объекта (обычно относительно другого SceneNode) вместе с реальным объектом.
Управление объектами
Как управляются SceneNode? С помощью SceneManager . Этот класс создает и отслеживает каждый SceneNode в вашей сцене. Вы можете запросить конкретный SceneNode (обычно идентифицируемый строковым именем, таким как «Player» или «Table») или список всех узлов.
Рисуя мир
Это должно быть довольно очевидно к настоящему времени. Просто пройдите каждый SceneNode в сцене и попросите рендерера нарисовать его в нужном месте. Вы можете нарисовать его в нужном месте, если рендерер сохранит преобразования объекта перед его рендерингом.
Обнаружение столкновений
Это не всегда тривиально. Обычно вы можете запросить сцену о том, какой объект находится в определенной точке пространства, или какие объекты пересекает луч. Таким образом, вы можете создать луч из своего игрока в направлении движения и спросить у менеджера сцены, какой объект пересекает первый луч. Затем вы можете переместить игрока на новую позицию, переместить его на меньшую величину (чтобы он оказался рядом со сталкивающимся объектом) или вообще не перемещать его. Убедитесь, что эти запросы обрабатываются отдельными классами. Им следует запросить у SceneManager список узлов SceneNode, но еще одна задача - определить, охватывает ли этот узел SceneNode точку в пространстве или пересекается с лучом. Помните, что SceneManager только создает и хранит узлы.
Итак, что такое игрок и что такое мир?
Player может быть классом, содержащим SceneNode, который, в свою очередь, содержит модель для рендеринга. Вы перемещаете игрока, изменяя положение узла сцены. Мир - это просто пример SceneManager. Он содержит все объекты (через SceneNodes). Вы обрабатываете обнаружение столкновений, выполняя запросы о текущем состоянии сцены.
Это далеко не полное или точное описание того, что происходит внутри большинства двигателей, но оно должно помочь вам понять основы и то, почему важно соблюдать принципы ООП, подчеркнутые SOLID . Не соглашайтесь с тем, что реструктурировать код слишком сложно или что он вам не поможет. Вы выиграете намного больше в будущем, тщательно разработав свой код.
источник
Почему вы хотите избежать этого? Следует избегать циклических зависимостей, если вы хотите создать класс многократного использования. Но Player - это не тот класс, который вообще нужно многократно использовать. Хотели бы вы когда-нибудь использовать плеер без мира? Возможно нет.
Помните, что классы - это не более чем наборы функций. Вопрос в том, как разделить функциональность. Делай все, что тебе нужно. Если вам нужен круговой упадок, так тому и быть. (Кстати, то же самое относится и к любым функциям ООП. Кодируйте вещи так, чтобы они служили цели, а не просто слепо следуйте парадигмам.)
Отредактируйте
Хорошо, чтобы ответить на вопрос: вы можете избежать того, что Игроку нужно знать Мир для проверки столкновений, используя обратные вызовы:
Вид физики, который вы описали в этом вопросе, может быть обработан миром, если вы выставите скорость сущностей:
Однако обратите внимание, что вам, вероятно, рано или поздно понадобится зависимость от мира, то есть когда вам нужна функциональность мира: вы хотите знать, где находится ближайший враг? Вы хотите знать, как далеко находится следующий выступ? Зависимость это есть.
источник
render(World)
. Спор идет о том, должен ли весь код быть помещен в один класс, или же код должен быть разделен на логические и функциональные блоки, которые затем легче поддерживать, расширять и управлять ими. Кстати, удачи в повторном использовании этих менеджеров компонентов, физических движков и менеджеров ввода, все они умно недифференцированы и полностью связаны.Ваш текущий дизайн, кажется, идет вразрез с первым принципом дизайна SOLID .
Этот первый принцип, называемый «принципом единой ответственности», как правило, является хорошим руководством, которому нужно следовать, чтобы не создавать монолитные, полезные объекты, которые всегда будут вредить вашему дизайну.
Конкретизируя, ваш
World
объект отвечает как за обновление и поддержание состояния игры, так и за рисование всего.Что если ваш код рендеринга изменится / должен измениться? Почему вы должны обновить оба класса, которые на самом деле не имеют ничего общего с рендерингом? Как уже сказал Лиосан, вы должны иметь
Renderer
.Теперь, чтобы ответить на ваш актуальный вопрос ...
Есть много способов сделать это, и это только один из способов развязки:
Object
s, в котором находится игрок, но он не зависит от класса игрока (для этого используйте наследование).InputManager
.Renderer
Рисует все объекты.источник
health
которым имеет только этот экземплярPlayer
).Игрок должен спросить мир о таких вещах, как обнаружение столкновений. Чтобы избежать циклической зависимости, не нужно, чтобы Мир зависел от Игрока. Мир должен знать, где он рисует сам: вы, вероятно, хотите, чтобы абстракция была удалена, возможно, со ссылкой на объект Camera, который, в свою очередь, может содержать ссылку на некоторый объект для отслеживания.
То, что вы хотите избежать с точки зрения циклических ссылок, это не столько хранение ссылок друг на друга, сколько обращение к друг другу явно в коде.
источник
Всякий раз, когда два разных типа объектов могут спрашивать друг друга. Они будут зависеть друг от друга, так как им нужно хранить ссылку на другую для вызова ее методов.
Вы можете избежать круговой зависимости, попросив Мир спросить Игрока, но Игрок не может спросить Мир, или наоборот. Таким образом, в Мире есть ссылки на Игроков, но игрокам не нужны ссылки на Мир. Или наоборот. Но это не решит проблему, потому что Мир должен будет спросить игроков, есть ли у них что-то спросить, и сказать им в следующем раунде ...
Таким образом, вы не можете обойти эту «проблему», и я думаю, что нет необходимости беспокоиться об этом. Делайте дизайн глупо простым, пока вы можете.
источник
Разбирая подробности об игроке и мире, у вас есть простой случай, когда вы не хотите вводить циклическую зависимость между двумя объектами (что в зависимости от вашего языка может даже не иметь значения, см. Ссылку в комментарии Fuhrmanator). Существует по крайней мере два очень простых структурных решения, которые применимы к этой и аналогичным проблемам:
1) Ввести одноплодной шаблон в свой мировой класс . Это позволит игроку (и любому другому объекту) легко находить объект мира без дорогостоящих поисков или постоянных ссылок. Суть этого паттерна в том, что у класса есть статическая ссылка на единственный экземпляр этого класса, который устанавливается при создании экземпляра объекта и очищается при его удалении.
В зависимости от языка разработки и сложности, которую вы хотите, вы можете легко реализовать это как суперкласс или интерфейс и повторно использовать его для многих основных классов, которые, как вы ожидаете, не будут иметь более одного в вашем проекте.
2) Если язык, на котором вы разрабатываете, поддерживает его (многие это делают), используйте Weak Reference . Это ссылка, которая не влияет на такие вещи, как сборка мусора. Это полезно именно в этих случаях, просто не делайте никаких предположений о том, существует ли еще объект, на который вы ссылаетесь слабо.
В вашем конкретном случае ваши игроки могут иметь слабую ссылку на мир. Преимущество этого (как и в случае синглтона) состоит в том, что вам не нужно искать какой-либо объект мира в каждом кадре или иметь постоянную ссылку, которая будет препятствовать процессам, на которые влияют циклические ссылки, такие как сборка мусора.
источник
Как говорили другие, я думаю, что вы
World
делаете одну вещь слишком много: она пытается одновременно содержать игруMap
(которая должна быть отдельной сущностью) и бытьRenderer
одновременно.Поэтому создайте новый объект (который
GameMap
, возможно, называется ) и сохраните в нем данные уровня карты. Напишите в нем функции, которые взаимодействуют с текущей картой.Тогда вам также нужен
Renderer
объект. Вы можете сделать этотRenderer
объект вещью, которая содержитGameMap
иPlayer
(иEnemies
), и также рисует их.источник
Вы можете избежать циклических зависимостей, не добавляя переменные в качестве членов. Используйте статическую функцию CurrentWorld () для игрока или что-то в этом роде. Не изобретайте интерфейс, отличный от того, который уже реализован в World, хотя это совершенно не нужно.
Также возможно уничтожить ссылку до / во время уничтожения объекта игрока, чтобы эффективно остановить проблемы, вызванные циклическими ссылками.
источник