Проблемы объектно-ориентированных приложений в разработке игр

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

Скажем, у меня есть класс Player. Этот класс Player выполняет такие действия, как изменение своего положения в игровом мире. Я вызываю этот метод warp(), который принимает экземпляр класса Position в качестве параметра для изменения внутренней позиции Player. С точки зрения объектно-ориентированного программирования это имеет для меня полный смысл, потому что я прошу игрока что-то «сделать».

Проблема возникает, когда мне нужно делать другие вещи в дополнение к простому изменению положения игроков. Например, скажем, мне нужно отправить это событие деформации другим игрокам в онлайн-игре. Должен ли этот код также находиться в методе warp() Player? Если нет, то я бы представил объявление какого-то вторичного метода, скажем, в классе сервера, например warpPlayer(player, position). Кажется, что это сводит все, что делает игрок, к серии геттеров и сеттеров, или я просто ошибаюсь? Это что-то совершенно нормальное? Я бесчисленное количество раз читал, что класс, который представляет все как серию геттеров/сеттеров, указывает на довольно плохую абстракцию (используется как структура данных вместо класса).

Та же проблема возникает, когда вам нужно сохранить данные, сохранив их в файл. Поскольку «сохранение» проигрывателя в файл находится на другом уровне абстракции, чем класс Player, имеет ли смысл иметь метод save() в классе проигрывателя? Если нет, то внешнее объявление типа savePlayer(player) означает, что методу savePlayer потребуется способ получить все необходимые данные из класса Player, что в конечном итоге приведет к раскрытию всей закрытой реализации класса.

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

Заранее спасибо, надеюсь, я не слишком похож на идиота. Для тех, кому действительно нужно знать языки, связанные с этим дизайном, это Java на стороне сервера и ActionScript 3 на стороне клиента.


person suinswofi    schedule 25.12.2009    source источник
comment
StackOverflow не очень подходит для ответа на вопросы такого типа. Как вы заметили, объектно-ориентированное программирование оказалось довольно успешным, поэтому я предлагаю решить ваши проблемы с ним, прочитав несколько книг и написав немного кода.   -  person    schedule 26.12.2009
comment
Я прочитал несколько книг, в том числе «Полный код», «Чистый код», «Практика программирования» и некоторые другие, которые не могу вспомнить. Если какой-либо из них напрямую касается этой темы, пожалуйста, дайте мне знать. Я просматривал их несколько раз и не видел ничего существенного.   -  person suinswofi    schedule 26.12.2009
comment
Я читал CoComp и TPOP (очень хорошо) - Чистый код, который я бы не стал трогать щеткой. Но ни один из них на самом деле не об ОО. В наши дни необычно встретить кого-то, кто не использует преимущества ООП - мой собственный код всегда был написан в стиле ОО (и я делаю это уже 30 лет), поэтому я, вероятно, не тот человек, попросите порекомендовать книгу, но мне всегда нравились произведения Грэди Буча.   -  person    schedule 26.12.2009
comment
Ну, я также читал объектно-ориентированное программирование с помощью Java, поэтому я понимаю всю терминологию, лежащую в основе методов объектно-ориентированного программирования, и даже читаю шаблоны проектирования для чайников. Я понимаю все это, но ни один из них не решает эту проблему, которую я бы назвал основной проблемой ООП.   -  person suinswofi    schedule 26.12.2009


Ответы (7)


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

Я думаю, вам следует использовать предложенный вами подход warpPlayer(player, position). Он поддерживает класс Player в чистоте. Если вы не хотите передавать игрока в функцию, возможно, вы могли бы иметь класс PlayerController, содержащий объект Player и метод warp(Position p). Таким образом, вы можете добавить публикацию событий в контроллер и не включать ее в модель.

Что касается сохранения плеера, я бы сделал это, заставив Player реализовать какой-то интерфейс сериализации. Класс player отвечает за сериализацию и десериализацию самого себя, а какой-то другой класс будет отвечать за запись сериализованных данных в/из файла.

person Tom Dalling    schedule 27.12.2009
comment
так, например, может быть метод, называемый сериализацией, который возвращает пару ключ-значение, содержащую состояние класса, которое затем может быть восстановлено с помощью другого метода или конструктора, принимающего пару ключ-значение. Меня беспокоило то, что вся эта сериализация была повсюду вокруг программы, но я не думал о том, чтобы просто иметь в ней часть сериализации, а затем фактический вывод в файл/базу данных выполнялся где-то еще. Спасибо за этот совет. - person suinswofi; 28.12.2009

Советую не опасаться того, что игрок будет классом геттеров и сеттеров. Что вообще такое объект? Это компиляция атрибутов и поведения. На самом деле, чем проще ваши классы, тем больше преимуществ ООП вы получите в процессе разработки.

Я бы разбил ваши задачи/функции на такие классы:

Игрок:

  • имеет атрибут хитпойнтов
  • имеет атрибут позиции
  • может ходить по (позиция), запуская события «ходьба»
  • может лечить(хитпойнты)
  • может получать урон (очки жизни), вызывая событие "isHurt"
  • можно проверить на живость, например метод isAlive()

Fighter расширяет Player (вы должны иметь возможность превратить Player в Fighter, когда это необходимо):

  • имеет силу и другие боевые параметры для расчета урона
  • может атаковать () запуская событие «атака»

Мир отслеживает всех игроков:

  • прослушивает события «прогулки» (и предотвращает незаконные движения)
  • слушает события «isHurt» (и проверяет, живы ли они)

Battle обрабатывает сражения между двумя бойцами:

  • конструктор с двумя бойцами в качестве параметров (вы хотите построить бой только между игроками, которые действительно сражаются друг с другом)
  • слушает события «атаки» от обоих игроков, вычисляет урон и выполняет метод takeDamage защищающегося игрока

PlayerPersister расширяет AbstractPersister:

  • сохраняет состояние игрока в базе данных
  • восстанавливает состояние игрока из базы данных

Конечно, разбивка вашей игры будет намного сложнее, но я надеюсь, что это поможет вам начать думать о проблемах с точки зрения «более ООП» :)

person Anton N    schedule 25.12.2009
comment
Да, это похоже на то, что я делаю прямо сейчас. Мой класс Player — это просто куча get/sets, а затем у меня есть еще один метод в классе Game, который имеет warpPlayer(player, newposition). Просто не кажется, что это должно быть лучшим решением. Я не могу это объяснить. - person suinswofi; 26.12.2009
comment
Нет, в моем примере Мировой класс вообще не может перемещать Игрока. Игрок перемещается сам с помощью метода walk() (вызываете метод walk() не из Мира, а из UI). И World может только помешать игроку перейти в недопустимую позицию (это только, например, вы можете реализовать свою собственную логику того, как ходьба игрока взаимодействует с миром), прослушивая события ходьбы игрока. - person Anton N; 26.12.2009

Я, вероятно, подумал бы о том, чтобы иметь объект Game, который отслеживает объект игрока. Таким образом, вы можете сделать что-то вроде game.WarpPlayerTo(WarpLocations.Forest); Если есть несколько игроков, возможно, передайте объект игрока или руководство с ним. Я чувствую, что вы все еще можете оставить его ООП, и я думаю, что игровой объект решит большинство ваших проблем.

person Dested    schedule 25.12.2009
comment
Это все еще было бы OO, но мне все еще кажется, по крайней мере, что класс игрока просто превращается в огромную серию геттеров и сеттеров. Представьте, что вы нападаете на игрока. Мне нужен был бы такой метод, как player.getVitals().setHP(int) или player.getVitals().damageHP(int), а также еще один вторичный метод, который на самом деле вычисляет ущерб, наносимый злоумышленником жертве. Это приводит к тому, что Player по-прежнему остается серией геттеров/сеттеров. Или, возможно, сделайте это: player.attack(attackerPlayer), в котором выполняются все внутренние вычисления. В этом случае нет возможности отправить обновление по сети. - person suinswofi; 26.12.2009

Проблемы, которые вы описываете, относятся не только к дизайну игр, но и к архитектуре программного обеспечения в целом. Обычный подход заключается в использовании механизмов Injection Dependency Injection (DI) и Inversion of Control (IoC). Короче говоря, вы пытаетесь получить доступ к локальному типу Service из ваших объектов, чтобы, например, распространять какое-либо событие (например, деформацию), журнал и т. д.

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

person Aviad P.    schedule 25.12.2009
comment
В этом случае, независимо от того, была ли это служба или нет, фактически создающая эти объекты (пример: Player), объекту Player все равно потребуется ссылка на службу (при условии, что она не является статической), чтобы он мог вызывать эти функции. Разве это не тесно связывает объект Player с сервисным объектом? В этом случае warp() будет тесно связан, например, с методом service.sendWarpEvent(). Является ли это вообще приемлемой практикой OO? - person suinswofi; 26.12.2009
comment
Если ваши объекты используют интерфейс для службы и независимо создают экземпляр фактического объекта службы, вы достигаете разделения. Поскольку вы можете заменить реальную реализацию службы в любое время. Конечно, интерфейс должен оставаться прежним, но связанность измеряется реализациями, а не интерфейсами. - person Aviad P.; 26.12.2009

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

В этом случае я бы посоветовал вам спроектировать данные, которые необходимо синхронизировать между всеми клиентами, и хранить их в одном классе (например, GameState). Этот объект будет обрабатывать всю синхронизацию между разными ПК, а также позволит вашему локальному коду запрашивать изменения данных. Затем он будет «управлять» игровыми объектами (Player, EnemyTank и т. д.) из своего собственного состояния. [редактировать: причина этого в том, что сохранение этого состояния как можно меньшим и его эффективная передача между клиентами будет ключевой частью вашего дизайна. Хранение всего этого в одном месте делает это намного проще и побуждает вас помещать в этот класс только самое необходимое, чтобы ваши сообщения не раздувались ненужными данными]

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

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

person Jason Williams    schedule 25.12.2009
comment
В вашем примере, когда все данные, связанные с игрой, хранятся в одном классе GameState, мне все равно нужно было бы определить функции в этом классе, чтобы выполнить весь процесс, который мне понадобится для моих объектов, таких как Player. Это по-прежнему будет создавать повторяющиеся функции, и все мои классы станут структурами данных. Из того, что я могу сказать в вашем посте, вы определяете API функций из этого одного класса для работы со всеми данными, верно? - person suinswofi; 26.12.2009
comment
Да. Вы можете по-прежнему хранить данные в отдельных объектах, но всякий раз, когда вы хотите переместить Player, вы должны попросить GameState обновить его, а не напрямую запрашивать игрока. Вы также можете принудительно применить это использование, сделав методы Player закрытыми, но дружественными GameState, поэтому это единственный класс, который может их изменять. - person Jason Williams; 26.12.2009

Иногда хитрость в ООП заключается в понимании того, что такое объект и какова функциональность объекта. Я думаю, что часто нам довольно легко концептуально зацикливаться на таких объектах, как Player, Monster, Item и т. д., как на «объектах» в системе, а затем нам нужно создавать такие объекты, как Environment, Transporter и т. д., чтобы связать эти объекты вместе, и это может выйти из-под контроля в зависимости от того, как концепции работают вместе, и что нам нужно выполнить.

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

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

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

person GrayWizardx    schedule 25.12.2009
comment
Я бы связал это с классами Бога в Code Complete. Это в значительной степени случай, который я делаю для последних решений в моем исходном посте, который требует дублирования определений методов. В основном функция warp() Player является лишь частью общего процесса, а вторичный метод в классе God вызывает метод warp Player вместе с другими методами для таких вещей, как отправка обновления деформации другим игрокам и, возможно, вывод сообщение игроку о том, что вы были деформированы или что-то в этом роде. Это по-прежнему делает Player в основном классом геттеров и сеттеров. :/ - person suinswofi; 26.12.2009
comment
На самом деле игрок был бы просто игровым объектом без понятия, что это игрок, камень или монстр, это просто игровой объект. Нет никакой реальной проблемы с классом сумки в вашем дизайне, если это его предполагаемая функция. Они отличаются от классов Бога, потому что сами они относятся не к контролирующему классу более высокого порядка, а скорее к классу сантехника более низкого порядка. - person GrayWizardx; 26.12.2009
comment
Наличие иерархии наследования, такой как GameObject, GamePlayer, GameNPC и т. д., по-прежнему не имеет ничего общего с этой проблемой. Я использую подобную иерархию для своего движка 2D-графики, где методы объектно-ориентированного программирования работают безупречно. Когда мне нужно сделать что-то, затрагивающее несколько областей абстракции, это становится проблемой. - person suinswofi; 26.12.2009

Я хотел бы расширить последний абзац GrayWizardx, чтобы сказать, что не все объекты должны иметь одинаковый уровень сложности. Для вашего проекта вполне может подойти объект, представляющий собой простой набор свойств get/set. С другой стороны, важно помнить, что объекты могут представлять задачи или наборы задач, а не объекты реального мира.

Например, объект игрока может не отвечать за перемещение игрока, а вместо этого представлять его позицию и текущее состояние. Объект PlayerMovement может содержать логику для изменения положения игрока на экране или в игровом мире.

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

person Dan J    schedule 25.12.2009
comment
Спасибо за комментарии. Я думаю, что то, что вы сказали, в значительной степени касается именно моей проблемы. Мне кажется, что в некоторых случаях это жонглирование между инкапсуляцией и сцеплением. В моих примерах выше наличие слабосвязанного объекта Player делает его серией геттеров и сеттеров, нарушающих инкапсуляцию imo. Или используйте хороший инкапсулированный объект Player, который ссылается на другие служебные объекты или статические ссылки, чтобы выполнять другие необходимые действия, делая класс тесно связанным с его служебным объектом/глобальными методами, на которые ссылаются статические объекты. - person suinswofi; 26.12.2009