Питання архітектури гри / дизайн - створення ефективного двигуна, уникаючи глобальних випадків (гра C ++)


28

У мене виникло питання щодо ігрової архітектури: який найкращий спосіб спілкуватися між собою різними компонентами?

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

Я намагався створити гру з нуля (C ++, якщо це має значення), і натхненно спостерігав ігрове програмне забезпечення з відкритим кодом (Super Maryo Chronicles, OpenTTD та інші). Я зауважую, що багато з цих ігор-дизайнів використовують глобальні екземпляри та / або одиночні кнопки повсюдно (для таких речей, як черги візуалізації, менеджери організацій, відео-менеджери тощо). Я намагаюся уникати глобальних екземплярів та синглів та будувати двигун, який є максимально вільним, але я стикаюся з деякими перешкодами, які завдячують моєму недосвідченню в ефективному дизайні. (Частина мотивації цього проекту полягає у вирішенні цього питання :))

Я створив дизайн, де у мене є один головний GameCoreоб'єкт, у якого є члени, аналогічні глобальним екземплярам, ​​які я бачу в інших проектах (тобто, він має менеджер вводу даних, відеоменеджер, GameStageоб'єкт, який контролює всі об'єкти та гру для будь-якого етапу зараз завантажується тощо). Проблема полягає в тому, що оскільки все є централізованим в GameCoreоб'єкті, у мене немає простого способу спілкування різних компонентів між собою.

Наприклад, дивлячись на Super Maryo Chronicles, наприклад, коли компонент гри потребує зв'язку з іншим компонентом (тобто ворожий об’єкт хоче додати себе до черги візуалізації, яку потрібно намалювати на етапі візуалізації), він просто спілкується з глобальний екземпляр.

Для мене я повинен змусити мої ігрові об'єкти передавати відповідну інформацію назад GameCoreоб'єкту, щоб GameCoreоб'єкт міг передавати цю інформацію іншому компоненту системи, який її потребує (тобто для вищезазначеної ситуації кожен ворожий об'єкт передасть їх інформацію про візуалізацію назад до GameStageоб'єкта, який би зібрав це все і передав би назад GameCore, що в свою чергу передасть його в відео-менеджер для надання). Це здається справді жахливим дизайном, як це є, і я намагався придумати це рішення. Мої думки щодо можливих конструкцій:

  1. Глобальні екземпляри (дизайн Super Maryo Chronicles, OpenTTD тощо)
  2. Наявність GameCoreоб'єкта виступає посередником, за допомогою якого всі об'єкти спілкуються (поточний дизайн, описаний вище)
  3. Дайте покажчики компонентів всім іншим компонентам, з якими потрібно буде поговорити (тобто, у прикладі Маріо вище, ворожий клас мав би вказівник на відеооб'єкт, з яким він повинен поговорити)
  4. Розбийте гру на підсистеми - Наприклад, в GameCoreоб’єкті є об’єкти менеджера, які керують зв’язком між об'єктами в їх підсистемі
  5. (Інші варіанти? ....)

Я уявляю, що варіант 4 вище є найкращим рішенням, але у мене виникають певні проблеми при його розробці ... можливо, тому, що я думав з точки зору дизайну, який я бачив, в якому використовуються глобалі. Складається враження, що я приймаю ту саму проблему, яка існує в моєму теперішньому дизайні, і тиражую її в кожній підсистемі, тільки в меншому масштабі. Наприклад, GameStageописаний вище об'єкт певною мірою намагається це зробити, але GameCoreоб'єкт все ще бере участь у процесі.

Хтось може запропонувати тут будь-які поради щодо дизайну?

Спасибі!


1
Я розумію ваш інстинкт, що одинаки не є чудовим дизайном. На мій досвід, вони були найпростішим способом керування спілкуванням між системами
Еммет Батлер

4
Додавання як коментар, оскільки я не знаю, чи це найкраща практика. У мене є центральний GameManager, який складається з таких підсистем, як InputSystem, GraphicsSystem тощо. Кожна підсистема приймає GameManager як параметр у конструкторі та зберігає посилання на приватного члена класу. У цей момент я можу звернутися до будь-якої іншої системи, звернувшись до неї через посилання GameManager.
Inisheer

Я змінив теги, оскільки це питання стосується коду, а не ігрового дизайну.
Клайм

ця нитка трохи стара, але у мене точно така ж проблема. Я використовую OGRE і намагаюся використовувати найкращий спосіб, на мою думку, варіант №4 - це найкращий підхід. Я створив щось на кшталт Advanced Ogre Framework, але це не дуже модульно. Я думаю, що мені потрібна обробка вводу підсистеми, яка отримує лише удари клавіатури та рухи миші. Я не розумію, як я можу створити такий менеджер "комунікації" між підсистемами?
Dominik2000

1
Привіт @ Dominik2000, це веб-сайт із питань запитань, а не форум. Якщо у вас є питання, ви повинні розмістити фактичне запитання, а не відповідь на існуюче. Докладнішу інформацію див. У файлі .
Джош

Відповіді:


19

Щось ми використовуємо в своїх іграх для організації наших глобальних даних - це модель дизайну ServiceLocator . Перевага цього шаблону порівняно з шаблоном Singleton полягає в тому, що реалізація ваших глобальних даних може змінюватися під час виконання програми. Також ваші глобальні об'єкти також можуть бути змінені під час виконання. Ще одна перевага полягає в тому, що простіше керувати порядком ініціалізації ваших глобальних об'єктів, що дуже важливо, особливо в C ++.

наприклад (код C #, який легко перекласти на C ++ або Java)

Скажемо, що у вас є інтерфейс резервного візуалізації, який має деякі загальні операції з рендерингу матеріалів.

public interface IRenderBackend
{
    void Draw();
}

І що у вас є реалізація за замовчуванням візуалізації за замовчуванням

public class DefaultRenderBackend : IRenderBackend
{
    public void Draw()
    {
        //do default rendering stuff.
    }
}

У деяких проектах здається, що легітимним є можливість отримати доступ до бекенда візуалізації в усьому світі. У шаблоні Singleton це означає, що кожна реалізація IRenderBackend повинна бути реалізована як унікальний глобальний екземпляр. Але використання шаблону ServiceLocator цього не вимагає.

Ось як:

public class ServiceLocator<T>
{
    private static T currGlobalInstance;

    public static T Service
    {
        get { return currGlobalInstance; }
        set { currGlobalInstance = value; }
    }
}

Для того, щоб мати доступ до свого глобального об'єкта, вам потрібно спочатку його активізувати.

//somewhere during program initialization
ServiceLocator<IRenderBackend>.Service = new DefaultRenderBackend();

//somewhere else in the code
IRenderBackend currentRenderBackend = ServiceLocator<IRenderBackend>.Service;

Для того, щоб продемонструвати, як реалізація може змінюватися під час виконання, скажімо, що у вашій грі є міні-гра, де візуалізація є ізометричною, а ви реалізуєте IsometricRenderBackend .

public class IsometricRenderBackend : IRenderBackend
{
    void draw()
    {
        //do rendering using an isometric view
    }
}

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

ServiceLocator<IRenderBackend>.Service = new IsometricRenderBackend();

Ще одна перевага полягає в тому, що ви також можете користуватися нульовими послугами. Наприклад, якщо ми мали ISoundManager обслуговування і користувач хоче вимкнути звук, ми могли б просто реалізувати NullSoundManager , що нічого не робить , коли його методи називаються, так шляхом установки пошуку сервісу «s об'єкт служби до NullSoundManager об'єкта , який ми могли б досягти це результат майже з будь-якою роботою.

Підводячи підсумок, іноді може бути неможливо усунути глобальні дані, але це не означає, що ви не можете їх правильно організувати та об'єктно орієнтованим чином.


Я раніше це вивчав, але фактично не втілював це в жодному з моїх проектів. Цього разу я планую. Дякую :)
Awesomania

3
@Erevis Отже, ви в основному описуєте глобальну посилання на поліморфний об'єкт. У свою чергу, це просто подвійне непряме (покажчик -> інтерфейс -> реалізація). У C ++ він може бути легко реалізований як std::unique_ptr<ISomeService>.
Shadows In Rain

1
Ви можете змінити стратегію ініціалізації, щоб "ініціалізуватись при першому доступі", і уникнути необхідності виділення деякої зовнішньої послідовності коду та натискання послуг на локатор. Ви можете додати список "залежить від" до служб, так що коли ініціалізується, він автоматично налаштовує інші необхідні йому послуги, а не молиться, щоб хтось згадав це зробити в main.cpp. Гарна відповідь з гнучкістю для майбутніх налаштувань.
Патрік Х'юз

4

Існує багато способів розробити ігровий движок, і це дійсно все зводиться до уподобань.

Щоб вийти з основ, деякі розробники вважають за краще розробити його як піраміду, де є якийсь верхній ядровий клас, який часто називають ядром, ядром або рамковим класом, який створює, володіє і ініціалізує низку таких підсистем як аудіо, графіка, мережа, фізика, AI, так і управління завданнями, об'єктами та ресурсами. Як правило, ці підсистеми піддаються вам цим рамковим класом, і зазвичай ви передаєте цей рамковий клас власним класам як аргумент конструктора, де це доречно.

Я вважаю, що ви на правильному шляху, думаючи про варіант №4.

Майте на увазі, якщо мова йде про самому спілкуванні, це не завжди має означати прямий виклик функції. Існує багато непрямих способів комунікації, будь то через якийсь непрямий метод використання Signal and Slotsчи використання Messages.

Іноді в іграх важливо допустити, щоб дії відбувалися асинхронно, щоб наш цикл ігор рухався якомога швидше, щоб частота кадрів була неозброєною. Гравці не люблять повільні та похмурі сцени, тому ми мусимо знайти способи тримати речі для них, але тримати логіку, але перевіряти і замовляти теж. Хоча асинхронні операції мають своє місце, вони також не є відповіддю для кожної операції.

Просто знайте, що у вас буде поєднання синхронного та асинхронного зв'язку. Виберіть те, що підходить, але знайте, що вам потрібно підтримувати обидва стилі серед ваших підсистем. Підтримка дизайну для обох послужить вам в майбутньому.


1

Ви просто повинні переконатися, що немає зворотних чи циклічних залежностей. Наприклад, якщо у вас є клас Core, і це Coreмає список Level, і список Levelмає список Entity, тоді дерево залежності має виглядати так:

Core --> Level --> Entity

Таким чином, з огляду на це початкове дерево залежностей, ви ніколи не повинні Entityзалежати від Levelабо Core, і Levelніколи не повинні залежати від того Core. Якщо Levelабо Entityпотрібно мати доступ до даних, які вище в дереві залежностей, його слід передавати як параметр за посиланням.

Розглянемо наступний код (C ++):

class Core;
class Entity;
class Level;

class Level
{
    public:
        Level(Core& coreIn) : core(coreIn) {}

        Core& core;
}

class Entity
{
    public:
        Entity(Level& levelIn) : level(levelIn) {}

        Level& level;
}

Використовуючи цю техніку, ви можете побачити , що кожен Entityмає доступ до Level, і Levelмає доступ до Core. Зауважте, що кожен Entityзберігає посилання на одне і те ж Level, витрачаючи пам'ять. Помітивши це, слід поставити під сумнів, чи Entityдійсно кожному потрібен доступ до Level.

На мій досвід, є або A) Дійсно очевидне рішення, щоб уникнути зворотних залежностей, або B) Не існує жодного можливого способу уникнути глобальних екземплярів та одинаків.


Я щось пропускаю? Ви згадуєте, що "ніколи не слід мати сутність залежно від рівня", але потім ви описуєте це ctor як "Entity (Level & levelIn)". Я розумію, що залежність передається посилання, але це все-таки залежність.
Адам Нейлор

@AdamNaylor Справа в тому, що іноді вам справді потрібні зворотні залежності, і ви можете уникнути глобальних, передаючи посилання. В цілому, хоча найкраще цілком уникати цих залежностей, і не завжди зрозуміло, як це зробити.
Яблука

0

Отже, в основному ви хочете уникнути глобального стану, що змінюється ? Ви можете зробити його або локальним, незмінним, або зовсім не станом. Останній є найбільш ефективним та гнучким, imo. Він відомий як приховування іплементації.

class ISomeComponent // abstract base class
{
    //...
};

extern ISomeComponent & g_SomeComponent; // will be defined somewhere else;

0

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

struct sEnvironment
{
    owning<iAudio*> m_Audio;
    owning<iRenderer*> m_Renderer;
    owning<iGameLevel*> m_GameLevel;
    ...
}

І передайте його навколо як невласницький сирий вказівник sEnvironment*. Тут покажчики вказують на інтерфейси, тому зв'язок зменшується аналогічно порівняно з службовим локатором. Однак усі сервіси знаходяться в одному місці (що може бути, а може і не корисно). Це просто інший підхід.

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