Как я могу иметь объекты, взаимодействующие и взаимодействующие друг с другом без навязывания иерархии?

9

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

Познакомьтесь с BoxPong , очень простой игрой, которую я сделал, чтобы познакомиться с разработкой объектно-ориентированных игр. Перетащите коробку, чтобы контролировать мяч и собирать желтые вещи.
Создание BoxPong помогло мне сформулировать, среди прочего, фундаментальный вопрос: как я могу иметь объекты, которые взаимодействуют друг с другом без необходимости «принадлежать» друг другу? Другими словами, есть ли способ для объектов не быть иерархическими, а вместо этого сосуществовать? (Я буду вдаваться в подробности ниже.)

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

Особенно в простых играх, таких как BoxPong, ясно, что существует или должно быть несколько объектов, сосуществующих на одном уровне. Там есть коробка, есть шар, есть предмет коллекционирования. Все, что я могу выразить на объектно-ориентированных языках, хотя - или так кажется - это строгие отношения HAS-A . Это делается с помощью переменных-членов. Я не могу просто начать ballи позволить этому делать свое дело, мне нужно, чтобы он постоянно принадлежал другому объекту. Я настроил его так , чтобы главный объект игры имеет коробку, а коробка в свою очередь , имеет мяч, и имеет надрубленной счетчик. Каждый объект также имеетupdate()метод, который вычисляет положение, направление и т. д., и я иду аналогичным образом: я вызываю метод обновления основного игрового объекта, который вызывает методы обновления всех его потомков, а они в свою очередь вызывают методы обновления всех своих потомков. Это единственный способ создать объектно-ориентированную игру, но я чувствую, что это не идеальный способ. В конце концов, я бы не думал, что мяч принадлежит к ящику, а скорее находится на одном уровне и взаимодействует с ним. Я полагаю, что этого можно добиться, превратив все игровые объекты в переменные-члены основного игрового объекта, но я не вижу, что это решает что-либо. Я имею в виду ... оставляя в стороне очевидный беспорядок, как мог бы быть шар и коробка узнать друг друга , то есть взаимодействовать?

Есть также проблема объектов, нуждающихся в передаче информации между собой. У меня довольно большой опыт написания кода для SNES, где у вас есть доступ практически ко всей оперативной памяти. Скажем, вы делаете своего собственного врага для Super Mario World , и вы хотите, чтобы он убрал все монеты Марио, а затем просто сохранил ноль для адреса $ 0DBF, без проблем. Нет никаких ограничений на то, что враги не могут получить доступ к статусу игрока. Я думаю, что я был избалован этой свободой, потому что с C ++ и тому подобным я часто задаюсь вопросом, как сделать значение доступным для некоторых других объектов (или даже глобальных).
На примере BoxPong, что, если бы я хотел, чтобы мяч отскакивал от краев экрана? widthи heightявляются свойствами Gameкласса,ballиметь доступ к ним. Я мог бы передавать такие значения (либо через конструкторы, либо через методы, где они необходимы), но это просто кричит о плохой практике.

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

Я слышал о «классах друзей» на C ++ и вроде знаю, как они работают, но если они представляют собой конечное решение, то почему я не вижу friendключевых слов, разбросанных по каждому отдельному проекту C ++, и почему концепция не существует в каждом языке ООП? (То же самое касается указателей на функции, о которых я только недавно узнал.)

Заранее спасибо за ответы любого рода - и еще раз, если есть часть, которая не имеет смысла для вас, дайте мне знать.

vvye
источник
2
Большая часть игровой индустрии перешла к архитектуре Entity-Component-System и ее вариациям. Это мышление, отличное от традиционных ОО-подходов, но оно работает хорошо и имеет смысл, когда в него вписывается концепция. Unity использует его. На самом деле, Unity просто использует компонент Entity-Component, но основан на ECS.
Данк
Проблема предоставления классам возможности сотрудничать друг с другом, не зная друг друга, решается с помощью шаблона проектирования Mediator. Вы смотрели на это?
Фурманатор

Ответы:

13

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

Это работает намного лучше, если есть некоторый объект «выше», который знает о них и может устанавливать взаимодействия между ними. Объект, который знает о двух одноранговых узлах, может связать их вместе посредством внедрения зависимости, или посредством событий, или посредством передачи сообщений (или любого другого механизма разъединения). Да, это приводит к некоторой искусственной иерархии, но это намного лучше, чем беспорядок спагетти, который вы получаете, когда вещи просто взаимодействуют вольно-невольно. Это только более важно в C ++, поскольку вам нужно что-то, чтобы владеть временем жизни объектов.

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

Telastyn
источник
2

На примере BoxPong, что, если бы я хотел, чтобы мяч отскакивал от краев экрана? Ширина и высота - свойства класса Game, и мне нужен шарик для доступа к ним.

Нет!

Я думаю, что главная проблема, с которой вы столкнулись, это то, что вы воспринимаете «объектно-ориентированное программирование» слишком буквально. В ООП объект представляет собой не «вещь», а «идею», которая означает, что «Мяч», «Игра», «Физика», «Математика», «Дата» и т. Д. Все являются допустимыми объектами. Также не требуется, чтобы объекты «знали» о чем-либо. Например, Date.Now().getTommorrow()спросите компьютер, какой сегодня день, примените тайные правила даты, чтобы выяснить дату завтрашнего дня, и верните ее вызывающему. DateОбъект не знает ни о чем еще, это нужно только запрашивает информацию по мере необходимости из системы. Кроме того, Math.SquareRoot(number)не нужно ничего знать, кроме логики того, как рассчитать квадратный корень.

Таким образом, в вашем примере, который я привел, «Мяч» не должен ничего знать о «Ящике». Коробки и мячики - совершенно разные идеи, и не имеют права разговаривать друг с другом. Но физический движок знает, что такое Box и Ball (или, по крайней мере, ThreeDShape), и он знает, где они находятся и что с ними должно происходить. Поэтому, если шар сжимается из-за холода, Физический Двигатель скажет этому экземпляру шара, что теперь он меньше.

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

А в вашем примере с Марио, что если вы находитесь в комнате испытаний, где прикосновение к врагу не истощает монеты Мариоса, а просто выбрасывает его из этой комнаты? За пределами области идей Марио или врага Марио должен потерять монеты, касаясь врага (фактически, если у Марио есть звезда неуязвимости, он убивает врага вместо этого). Таким образом, любой объект (домен / идея), который отвечает за то, что происходит, когда Марио касается врага, является единственным, который должен знать обо всем, и должен делать что угодно с любым из них (в той степени, в которой этот объект допускает внешние управляемые изменения). ).

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

Изучение того, как определить ваши доменные идеи в четкие, изолированные, четко определенные и простые в использовании блоки, будет самым большим фактором, насколько хорошо вы сможете использовать ООП.

Tezra
источник
1

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

Создание BoxPong помогло мне сформулировать, среди прочего, фундаментальный вопрос: как я могу иметь объекты, которые взаимодействуют друг с другом без необходимости «принадлежать» друг другу?

У меня довольно большой опыт написания кода для SNES, где у вас есть доступ практически ко всей оперативной памяти. Скажем, вы делаете своего собственного врага для Super Mario World, и вы хотите, чтобы он убрал все монеты Марио, а затем просто сохранил ноль для адреса $ 0DBF, без проблем.

Похоже, вы упускаете суть объектно-ориентированного программирования.

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

Что такое зависимость? Зависимость - это зависимость от чего-то другого. Когда вы храните ноль для адреса $ 0DBF, вы полагаетесь на тот факт, что этот адрес находится там, где расположены монеты Марио, и что монеты представлены в виде целого числа. Ваш пользовательский код врага зависит от кода, реализующего Марио и его монеты. Если вы сделаете изменение в том, где Марио хранит свои монеты в памяти, вы должны вручную обновить весь код, ссылающийся на ячейку памяти.

Объектно-ориентированный код - это все, чтобы ваш код зависел от абстракций, а не от деталей. Так что вместо

class Mario
{
    public:
        int coins;
}

ты бы написал

class Mario
{
    public:
        void LoseCoins();

    private:
        int coins;
}

Теперь, если вы хотите изменить то, как Марио хранит свои монеты с int на long или double, или хранить их в сети, или хранить в базе данных, или запустить какой-то другой длинный процесс, вы вносите изменения в одном месте: Класс Mario, и весь ваш другой код продолжает работать без изменений.

Поэтому, когда вы спрашиваете

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

Вы действительно спрашиваете:

Как я могу иметь код, который напрямую зависит друг от друга без каких-либо абстракций?

который не является объектно-ориентированным программированием.

Я предлагаю вам начать с прочтения всего здесь: http://objectmentor.com/omSolutions/oops_what.html, а затем найти в youtube все Роберта Мартина и посмотреть все это.

Мои ответы исходят от него, а некоторые прямо цитируются им.

Марк Мурфин
источник
Спасибо за ответ (и страница, на которую вы ссылаетесь; выглядит интересно). Я на самом деле знаю об абстракции и возможности повторного использования, но я полагаю, что не очень хорошо выразил это в своем ответе. Однако из приведенного вами примера кода я могу лучше проиллюстрировать свою точку зрения сейчас! Вы в основном говорите, что вражеский объект не должен делать mario.coins = 0;, но mario.loseCoins();, что хорошо и верно - но я хочу сказать, как враг может иметь доступ к marioобъекту в любом случае? marioбыть переменной-членом enemyмне не кажется правильным.
Ввод
Ну, простой ответ - передать Марио в качестве аргумента функции врагом. У вас может быть такая функция, как marioNearby () или attackMario (), которая будет принимать Марио в качестве аргумента. Таким образом, всякий раз, когда срабатывает логика, лежащая в основе взаимодействия Врага и Марио, вы вызываете врага.marioNearby (mario), который вызывает mario.loseCoins (); Позже вы можете решить, что есть класс врагов, которые заставляют Марио терять только одну монету или даже получать монеты. Теперь у вас есть одно место для внесения таких изменений, которое не вызывает побочных эффектов в другом коде.
Марк Мурфин
Передав Марио врагу, вы только что соединили его. Марио и Враг не должны знать, что другой - это даже вещь. Вот почему мы создаем объекты более высокого порядка, чтобы управлять соединением простых объектов.
Tezra
@Tezra Но тогда, разве эти объекты высшего порядка не могут быть повторно использованы вообще? Такое ощущение, что эти объекты действуют как функции, они существуют только для той процедуры, которую они демонстрируют.
Стив Шамайяр
@SteveChamaillard Каждая программа будет иметь хотя бы немного специфической логики, которая не имеет смысла ни в одной другой программе, но идея состоит в том, чтобы сохранить эту логику изолированной для нескольких классов высокого порядка. Если у вас есть классы марио, врагов и уровней, вы можете повторно использовать марио и врагов в других играх. Если вы связываете врага и марио прямо друг с другом, то любая игра, в которой она нужна, должна втягивать и другую.
Тезра
0

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

Он понимает, что такое «мастер игры, пожалуйста, пусть это случится».

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

Существуют синхронные и асинхронные варианты, обычно называемые шиной событий, шиной сообщений или очередью сообщений. Найдите их, чтобы определить, подходят ли они в вашем конкретном случае.

Hero Wanders
источник