Когда нескольким классам требуется доступ к одним и тем же данным, где эти данные должны быть объявлены?

39

У меня есть базовая 2D игра Tower Defense на C ++.

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

update():
  for each creep in creeps:
    creep.update()
  for each tower in towers:
    tower.update()
  for each missile in missiles:
    missile.update()

Объекты (крипы, башни и ракеты) хранятся в векторе указателей. Башни должны иметь доступ к вектору крипов и вектору ракет для создания новых ракет и определения целей.

Вопрос в том, где я объявляю векторы? Должны ли они быть членами класса Map и передаваться в качестве аргументов функции tower.update ()? Или объявлено глобально? Или есть другие решения, которые я пропускаю полностью?

Когда нескольким классам требуется доступ к одним и тем же данным, где эти данные должны быть объявлены?

сочный
источник
1
Глобальные участники считаются «уродливыми», но они быстрые и облегчают разработку, если это маленькая игра, это не проблема (ИМХО). Вы также можете создать внешний класс, который обрабатывает логику ( зачем вышкам нужны эти векторы) и имеет доступ ко всем векторам.
Джонатан Коннелл
-1 если это связано с программированием игры, то есть и пиццу тоже. Возьмите с собой несколько хороших книг по разработке программного обеспечения
Maik Semder
9
@Maik: Как дизайн программного обеспечения не связан с программированием игр? То, что это относится и к другим областям программирования, не делает его не по теме.
BlueRaja - Дэнни Пфлюгофт
@BlueRaja списки шаблонов проектирования программного обеспечения лучше подходят для SO, в конце концов это то, для чего они нужны. GD.SE предназначен для программирования игр, а не для разработки программного обеспечения
Maik Semder

Ответы:

53

Когда вам нужен один экземпляр класса в вашей программе, мы называем этот класс сервисом . Существует несколько стандартных методов реализации сервисов в программах:

  • Глобальные переменные . Это самый простой в реализации, но худший дизайн. Если вы используете слишком много глобальных переменных, вы быстро создадите модули, которые слишком сильно зависят друг от друга ( сильная связь ), что затрудняет отслеживание потока логики. Глобальные переменные не поддерживают многопоточность. Глобальные переменные усложняют отслеживание времени жизни объектов и загромождают пространство имен. Однако они являются наиболее эффективным вариантом, поэтому бывают случаи, когда их можно и нужно использовать, но используйте их с осторожностью.
  • Одиночки . Около 10-15 лет назад, одиночки были большой дизайн-шаблон , чтобы знать. Однако в настоящее время на них смотрят свысока. Они намного проще для многопоточности, но вы должны ограничить их использование одним потоком за раз, что не всегда то, что вы хотите. Отслеживание времени жизни так же сложно, как и с глобальными переменными. Типичный синглтон-класс будет выглядеть примерно так:

    class MyClass
    {
    private:
        static MyClass* _instance;
        MyClass() {} //private constructor
    
    public:
        static MyClass* getInstance();
        void method();
    };
    
    ...
    
    MyClass* MyClass::_instance = NULL;
    MyClass* MyClass::getInstance()
    {
        if(_instance == NULL)
            _instance = new MyClass(); //Not thread-safe version
        return _instance;
    
        //Note that _instance is *never* deleted - 
        //it exists for the entire lifetime of the program!
    }
    
  • Инъекция зависимости (DI) . Это просто означает передачу сервиса в качестве параметра конструктора. Служба уже должна существовать, чтобы передать ее в класс, поэтому две службы не могут полагаться друг на друга; в 98% случаев это то, что вы хотите (а для других 2% вы всегда можете создать setWhatever()метод и передать его позже) . Из-за этого у DI нет тех же проблем со связью, как у других опций. Его можно использовать с многопоточностью, поскольку каждый поток может просто иметь свой собственный экземпляр каждой службы (и делиться только теми, которые ему абсолютно необходимы). Это также делает код модульно-тестируемым, если вы заботитесь об этом.

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

    //Example of dependency injection
    class Tower
    {
    private:
        MissileCreationService* _missileCreator;
        CreepLocatorService* _creepLocator;
    public:
        Tower(MissileCreationService*, CreepLocatorService*);
    }
    
    //In order to create a tower, the creating-class must also have instances of
    // MissileCreationService and CreepLocatorService; thus, if we want to 
    // add a new service to the Tower constructor, we must add it to the
    // constructor of every class which creates a Tower as well!
    //This is not a problem in languages like C# and Java, where you can use
    // a framework to create an instance and inject automatically.
    

    Посмотрите эту страницу (из документации по Ninject, C # DI framework) для другого примера.

    Внедрение зависимостей является обычным решением этой проблемы, и именно этот ответ вы найдете наиболее высоко оцененным по таким вопросам на StackOverflow.com. DI - это тип инверсии контроля (IoC).

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

    Сервисные локаторы - это другая форма инверсии контроля (IoC). Обычно платформы, которые выполняют автоматическое внедрение зависимостей, также имеют указатель службы.

    XNA (среда программирования игр C # от Microsoft) включает в себя локатор служб; чтобы узнать больше об этом, смотрите этот ответ .


Кстати, ИМХО вышки не должны знать о крипах. Если вы не планируете просто перебирать список крипов для каждой башни, вы, вероятно, захотите реализовать некоторое нетривиальное разбиение пространства ; и такая логика не относится к классу башен.

BlueRaja - Дэнни Пфлугхофт
источник
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
Джош
Один из лучших, самых ясных ответов, которые я когда-либо читал. Отлично сработано. Я думал, что служба всегда должна быть разделена, хотя.
Никос
5

Я лично использовал бы здесь полиморфизм. Зачем иметь missileвектор, towerвектор и creepвектор ... когда все они вызывают одну и ту же функцию; update? Почему бы не иметь вектор указателей на некоторый базовый класс Entityили GameObject?

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

Одним из способов решения этой проблемы является система обмена сообщениями. Он towerможет отправить сообщение map(к которому у него есть доступ, может быть, ссылка на его владельца?), Что он ударил creep, а mapзатем сообщает, что creepего ударили. Это очень чисто и разделяет данные.

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

Коммунистическая утка
источник
1
Ваше предположение о полиморфизме на самом деле не актуально. Я храню их в отдельных векторах, так что я могу перебирать каждый тип индивидуально, например, в коде рисования (где я хочу, чтобы определенные объекты рисовались первыми) или в коде столкновения.
Сочные
Для моих целей карта действительно владеет объектами, поскольку карта здесь аналогична «уровню». Я рассмотрю вашу идею о сообщениях, спасибо.
Сочные
1
В игре производительность имеет значение. Таким образом, векторы одного и того же времени объекта имеют лучшую локальность ссылок. Кроме того, полиморфные объекты с виртуальными указателями имеют ужасную производительность, потому что они не могут быть встроены в цикл обновления.
Zan Lynx
0

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

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

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

Крайним примером этого подхода является архитектура системы сущностей .

Стив С
источник