Коли декілька класів потребують доступу до одних і тих же даних, де вони повинні бути оголошені?


39

У мене є основна 2D гра в оборону вежі в 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
Члени світу вважаються "потворними", але вони швидкі та полегшують розвиток, якщо це невелика гра, це не проблема (IMHO). Ви також можете створити розширений клас, який керується логікою ( чому вежі потребують цих векторів) і має доступ до всіх векторів.
Джонатан Коннелл

-1 якщо це пов’язано з ігровим програмуванням, то їсти піцу теж. Візьміть собі кілька хороших книжок з дизайну програмного забезпечення
Maik Semder

9
@Maik: Як дизайн програмного забезпечення не пов'язаний з ігровим програмуванням? Тільки тому, що це стосується і інших областей програмування, це не робить його поза темою.
BlueRaja - Danny Pflughoeft

@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) для іншого прикладу.

    Ін'єкція залежностей є звичайним рішенням цієї проблеми, і це відповідь, яку ви побачите на StackOverflow.com. DI - це тип інверсії управління (IoC).

  • Service Locator . В основному, просто клас, який містить екземпляр кожної служби. Ви можете це зробити за допомогою відображення або просто додати новий екземпляр до нього кожного разу, коли ви хочете створити нову послугу. Ви все ще маєте ту саму проблему, що і раніше - Як класи отримують доступ до цього локатора? - який можна вирішити будь-яким із перерахованих вище способів, але тепер це потрібно робити лише для вашого ServiceLocatorкласу, а не для десятків послуг. Цей метод також перевіряється на рівні одиниць, якщо ви дбаєте про такі речі.

    Локатори обслуговування - це ще одна форма інверсії управління (IoC). Зазвичай рамки, які виконують автоматичне введення залежності, також матимуть сервіс-локатор.

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


До речі, вежі ІМХО не повинні знати про повзання. Якщо ви не плануєте просто перебирати список повзуків для кожної вежі, ви, ймовірно, захочете здійснити деякий нетривіальний розділ простору ; і така логіка не належить до класу веж.


Коментарі не для розширеного обговорення; ця розмова переміщена до чату .
Джош

Один з найкращих, найясніших відповідей, які я коли-небудь читав. Молодці. Я думав, що службу завжди слід було ділити.
Нікос

5

Я особисто використовував би тут поліморфізм. Навіщо мати missileвектор, towerвектор і creepвектор ... коли всі вони називають одну і ту ж функцію; update? Чому б не мати вектор покажчиків на якийсь базовий клас Entityабо GameObject?

Я вважаю, що хороший спосіб проектувати - подумати, "чи має це сенс щодо власності"? Очевидно, що башта володіє способом оновлення, але чи карта володіє всіма об’єктами на ній? Якщо ви йдете на глобальний, ви говорите про те, що вежі та повзання нічого не належить? Глобальне зазвичай є поганим рішенням - воно сприяє поганим моделям дизайну, але з цим набагато простіше працювати. Подумайте про зважування "я хочу це закінчити?" і "чи хочу я чогось повторно використати?"

Один із способів цього - деяка форма системи обміну повідомленнями. towerМоже послати повідомлення на map(яке він має доступ до, можливо , посилання на його власника?) , Що він вдарив creep, а mapпотім каже , creepщо це був хіт. Це дуже чисто і відокремлює дані.

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


1
Ваша пропозиція щодо поліморфізму насправді не актуальна. У мене вони зберігаються в окремих векторах, тому я можу повторювати кожен тип окремо, наприклад, у коді малювання (де я хочу, щоб певні об'єкти були намальовані спочатку) або в коді зіткнення.
Соковитий

Для моїх цілей карта має власні сутності, оскільки карта тут аналогічна 'level'. Я розгляну вашу ідею щодо повідомлень, дякую.
Соковитий

1
У грі важлива продуктивність. Тож вектори одних і тих самих об’єктних часів мають кращу локальність відліку. Також поліморфні об'єкти з віртуальними покажчиками мають жахливу ефективність, оскільки їх не можна вписати в цикл оновлення.
Zan Lynx

0

Це випадок, коли суворе об'єктно-орієнтоване програмування (OOP) руйнується.

Відповідно до принципів ООП, слід групувати дані із пов’язаною поведінкою за допомогою класів. Але у вас є поведінка (орієнтація), яка потребує даних, не пов'язаних один з одним (вежі та повзучі). У цій ситуації багато програмістів намагатимуться пов’язати поведінку з частиною необхідних їй даних (наприклад, вежі обробляють націлювання, але не знають про повзання), але є й інший варіант: не групувати поведінку з даними.

Замість того, щоб робити поведінку орієнтації методом класового класу, зробіть її вільною функцією, яка приймає вежі та повзучі як аргументи. Це може зажадати оприлюднення більшості членів, які залишилися в баштах та повзучих класах, і це нормально. Приховування даних корисно, але це засіб, а не самоціль, і ви не повинні йому бути рабом. Крім того, приватні члени не є єдиним способом контролю доступу до даних - якщо дані не передаються у функцію і вони не є глобальними, це ефективно приховано від цієї функції. Якщо використання цієї методики дозволяє уникнути глобальних даних, ви, можливо, вдосконалюєте інкапсуляцію.

Крайнім прикладом такого підходу є архітектура системи сутності .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.