Вступ
У попередній статті ми зробили огляд процесу створення користувача аспекту і показали, як створити (велику частину) фронтенд функціонал. У цій статті ми продовжуємо будувати наш індивідуальний аспект, реалізуючи відповідні бекенд типи, реєструючи типи і налаштовуючи зв'язок фронтенд об'єктів з бекенд об'єктами. Це займе більшу частину цієї статті. У наступній статті ми розглянемо, як реалізувати завдання для опрацювання компонентів нашого аспекту.
Як нагадування про те, що ми маємо на увазі, ось діаграма архітектури з частини 1:
Створення бекенда
Одна з приємних речей Qt 3D полягає в тому, що вона здатна до дуже високої пропускної здатності. Це досягається за рахунок використання завдань, що виконуються в пулі потоків у бекенд. Щоб мати можливість зробити це без введення заплутаної мережі точок синхронізації (яка б обмежувала паралелізм), ми відтворюємо класичну комп'ютерну ситуацію про компроміс і жертву пам'яті на користь швидкості. Завдяки тому, що кожен аспект працює над власною копією даних, він може планувати завдання безпеки, знаючи, що ніщо інше не торкнеться його даних.
Це не так дорого, як це звучить. Бекенд-вузли не є похідними від QObject . Базовим класом для бекенд-вузлів є Qt3DCore::QBackendNode , який є досить легковажним класом . Крім того, зверніть увагу, що аспекти зберігають ті дані, які їм потрібні в бекенді. Наприклад, аспект анімації не дбає про те, який компонент Material має Entity , тому немає необхідності зберігати будь-які дані цього компонента. Навпаки, аспект рендерингу не стосується анімаційних кліпів або компонентів Animator.
У нашому маленькому для користувача аспекті у нас є тільки один тип фронтенд компонента, FpsMonitor . Логічно, у нас буде тільки один відповідний бекенд тип, який ми назвемо образно FpsMonitorBacken :
fpsmonitorbackend.h
class FpsMonitorBackend : public Qt3DCore::QBackendNode { public: FpsMonitorBackend() : Qt3DCore::QBackendNode(Qt3DCore::QBackendNode::ReadWrite) , m_rollingMeanFrameCount(5) {} private: void initializeFromPeer(const Qt3DCore::QNodeCreatedChangeBasePtr &change) override { // TODO: Implement me! } int m_rollingMeanFrameCount; };
Декларація класу дуже проста. Ми успадковуємо Qt3DCore::QBackendNode , як і слід очікувати; додаємо елемент даних, щоб відобразити інформацію з фронтенду компонента FpsMonitor ; та перевизначаємо віртуальну функцію initializeFromPeer() . Ця функція буде викликана одразу після того, як Qt 3D створить екземпляр нашого бекенд-типу. Аргумент дозволяє нам отримати дані, надіслані з відповідного фронтенду об'єкта, як ми незабаром побачимо.
Реєстрація типів
Тепер у нас є прості реалізації фронтенд та бекенд-компонентів. Наступний крок - зареєструвати їх за допомогою аспекту, щоб він знав як створити екземпляр бекенд-вузла щоразу, коли створюється фронтенд-вузол. Аналогічно знищення. Ми це робимо за допомогою проміжного помічника, відомого як співставник вузлів.
Щоб створити співставника вузлів, успадковуємося від Qt3DCore::QNodeMapper та перевизначаємо віртуальні функції для створення, пошуку та знищення бекенд-об'єктів на вимогу. Спосіб створення, зберігання, пошуку та знищення об'єктів повністю залежить від вас як розробника. Qt 3D не нав'язує вам будь-яку конкретну схему керування. Аспект рендерингу робить деякі досить химерні речі з керованими менеджерами пам'яті та вирівнюванням пам'яті для SIMD-типів, але тут ми можемо зробити щось набагато простіше.
Ми будемо зберігати вказівники на бекенд-вузли в QHash всередині CustomAspect та індексувати їх за допомогою Qt3DCore::QNodeId вузла. Ідентифікатор вузла використовується для однозначної ідентифікації даного вузла, навіть між фронтендом і всіма доступними бекендами. У Qt3DCore::QNode ідентифікатор доступний через функцію id() , тоді як для QBackendNode ви отримуєте доступ до нього через функцію peerId() . Для двох відповідних об'єктів, що представляють компонент, функції id() і peerId() повертають те саме значення QNodeId .
Давайте перейдемо до нього та додамо деяке сховище для бекенд-вузлів у CustomAspect разом із деякими допоміжними функціями:
customaspect.h
class CustomAspect : public Qt3DCore::QAbstractAspect { Q_OBJECT public: ... void addFpsMonitor(Qt3DCore::QNodeId id, FpsMonitorBackend *fpsMonitor) { m_fpsMonitors.insert(id, fpsMonitor); } FpsMonitorBackend *fpsMonitor(Qt3DCore::QNodeId id) { return m_fpsMonitors.value(id, nullptr); } FpsMonitorBackend *takeFpsMonitor(Qt3DCore::QNodeId id) { return m_fpsMonitors.take(id); } ... private: QHash<Qt3DCore::QNodeId, FpsMonitorBackend *> m_fpsMonitors; };
Тепер ми можемо реалізувати простий зіставник вузлів як:
fpsmonitorbackend.h
class FpsMonitorMapper : public Qt3DCore::QBackendNodeMapper { public: explicit FpsMonitorMapper(CustomAspect *aspect); Qt3DCore::QBackendNode *create(const Qt3DCore::QNodeCreatedChangeBasePtr &change) const override { auto fpsMonitor = new FpsMonitorBackend; m_aspect->addFpsMonitor(change->subjectId(), fpsMonitor); return fpsMonitor; } Qt3DCore::QBackendNode *get(Qt3DCore::QNodeId id) const override { return m_aspect->fpsMonitor(id); } void destroy(Qt3DCore::QNodeId id) const override { auto fpsMonitor = m_aspect->takeFpsMonitor(id); delete fpsMonitor; } private: CustomAspect *m_aspect; };
Щоб закінчити цей шматочок головоломки, нам потрібно розповісти про те, як ці типи та співставник пов'язані один з одним. Ми робимо це, викликаючи функцію шаблону QAbstractAspect::registerBackendType() , передаючи загальний покажчик співставнику, який буде створювати, знаходити та знищувати відповідні бекенд-вузли. Аргумент шаблону - це тип фронтенд-вузла, якого має викликатися цей співставник. Зручне місце для цього – у конструкторі CustomAspect. У нашому випадку це виглядає так:
customaspect.cpp
CustomAspect::CustomAspect(QObject *parent) : Qt3DCore::QAbstractAspect(parent) { // Register the mapper to handle creation, lookup, and destruction of backend nodes auto mapper = QSharedPointer<FpsMonitorMapper>::create(this); registerBackendType<FpsMonitor>(mapper); }
От і все! При такій реєстрації на місці, коли компонент FpsMonitor додається до дерева фронтенд-об'єктів (сцена), аспект шукатиме співставника вузлів для цього типу об'єкта. Тут він знайде наш зареєстрований об'єкт FpsMonitorMapper та викличе його функцію create() для створення бекенд-вузла та управління його зберігання. Аналогічна історія і зі знищенням (технічно це видалення зі сцени) фронтенд-вузла. Функція get() співставника використовується всередині, щоб мати можливість викликати віртуальні функції бекенд-вузла у відповідні моменти часу (наприклад, коли властивості повідомляють, що вони були змінені).
Зв'язок «перед-назад».
Тепер, коли ми можемо створити, отримати доступ і знищити бекенд-сайт будь-якого фронтенд-вузла, давайте подивимося, як ми можемо дозволити їм розмовляти один з одним. Є три основні моменти, коли фронтенд і бекенд вузли обмінюються даними один з одним:
- Ініціалізація - Коли наш бекенд-вузол створюється, ми отримуємо можливість ініціалізувати його даними, відправленими з фронтенд-вузла.
- Від фронтенд до бекенд - Зазвичай, коли властивості на фронтенд-вузлі змінюються, ми хочемо відправити нове значення якості бекенд-вузла, щоб він працював з актуальною інформацією.
- Від бекенд до фронтенд - Коли наші завдання обробляють дані, що зберігаються в бекенд-вузлах, може статися ситуація, що це призведе до оновлених значень, які мають бути відправлені у фронтенд-вузол.
Тут ми розглянемо перші два випадки. Третій випадок буде відкладено до наступної статті, коли ми представимо завдання.
Ініціалізація бекенд вузла
Весь зв'язок між фронтендом і бекендом об'єктами виконується шляхом відправки підкласу Qt3DCore::QSceneChanges . Вони аналогічні за своєю природою та концепцією для QEvent , але арбітр змін, який обробляє зміни, має можливість маніпулювати ними у разі конфліктів з кількох аспектів, переорієнтувати їх на пріоритет чи будь-які інші маніпуляції, які можуть знадобитися у майбутньому.
Для ініціалізації бекенд-вузла при створенні ми використовуємо Qt3DCore::QNodeCreatedChange , який є шаблоном, який ми можемо використовувати для обгортання певного типу даних. Коли Qt 3D хоче повідомити бекенд про початковий стан вашого фронтенда вузла, він викликає приватну віртуальну функцію QNode::createNodeCreationChange() . Ця функція повертає створене вузлом зміна, що містить будь-яку інформацію, до якої ми хочемо отримати доступ до бекенд-вузла. Ми повинні зробити це, скопіювавши дані, а не просто розіменувавши покажчик на фронтенд об'єкт, тому що до моменту, коли бекенд обробить запит, фронтенд об'єкт може бути видалений - тобто класична гонка даних. Для нашого простого компонента наша реалізація має такий вигляд:
fpsmonitor.h
struct FpsMonitorData { int rollingMeanFrameCount; };
fpsmonitor.cpp
Qt3DCore::QNodeCreatedChangeBasePtr FpsMonitor::createNodeCreationChange() const { auto creationChange = Qt3DCore::QNodeCreatedChangePtr<FpsMonitorData>::create(this); auto &data = creationChange->data; data.rollingMeanFrameCount = m_rollingMeanFrameCount; return creationChange; }
Зміна, створена нашим фронтенд-вузлом, передається на бекенд-вузол (через арбітр змін) та обробляється віртуальною функцією initializeFromPeer() :
fpsmonitorbackend.cpp
void FpsMonitorBackend::initializeFromPeer(const Qt3DCore::QNodeCreatedChangeBasePtr &change) { const auto typedChange = qSharedPointerCast<Qt3DCore::QNodeCreatedChange<FpsMonitorData>>(change); const auto &data = typedChange->data; m_rollingMeanFrameCount = data.rollingMeanFrameCount; }
Комунікація фронтенд з бекенд
На цьому етапі бекенд-вузол відображає початковий стан фронтенд-вузла. Але що, якщо користувач змінить якість у фронтенд-вузлі? Коли це станеться, наш бекенд-вузол зберігатиме застарілі дані.
Хорошою новиною є те, що з цим легко впоратися. Реалізація Qt3DCore::QNode дбає про першу половину проблеми за нас. Внутрішньо він прослуховує сигнали повідомлення Q_PROPERTY, і коли він бачить, що властивість змінилося, він створює для нас QPropertyUpdatedChange і відправляє його в арбітр змін, який, у свою чергу, доставляє його функції sceneChangeEvent() у бекенд-вузлі.
Таким чином, все, що нам потрібно зробити як авторам бекенд-вузла – перевизначити цю функцію, витягти дані з об'єкта зміни та оновити наш внутрішній стан. Вам часто захочеться якось помітити бекенд-вузол, щоб аспект знав, що його потрібно обробити у наступному кадрі. Тут, однак, ми просто оновимо стан, щоб відобразити останнє значення з фронтенду:
fpsmonitorbackend.cpp
void FpsMonitorBackend::sceneChangeEvent(const Qt3DCore::QSceneChangePtr &e) { if (e->type() == Qt3DCore::PropertyUpdated) { const auto change = qSharedPointerCast<Qt3DCore::QPropertyUpdatedChange>(e); if (change->propertyName() == QByteArrayLiteral("rollingMeanFrameCount")) { const auto newValue = change->value().toInt(); if (newValue != m_rollingMeanFrameCount) { m_rollingMeanFrameCount = newValue; // TODO: Update fps calculations } return; } } QBackendNode::sceneChangeEvent(e); }
Якщо ви не хочете використовувати вбудовану автоматичну відправку змін властивостей Qt3DCore::QNode , ви можете вимкнути її, обернувши випромінювання сигналу сповіщення властивості при виклику QNode::blockNotifications() . Це працює так само, як QObject::blockSignals() , за винятком того, що він блокує відправлення повідомлень тільки на базовий вузол, а не сам сигнал. Це означає, що інші з'єднання або прив'язки властивостей, які покладаються на ваші сигнали, як і раніше, працюватимуть.
Якщо ви блокуєте повідомлення за промовчанням таким чином, тоді вам необхідно надіслати їх, щоб переконатися, що базовий вузол має оновлену інформацію. Не соромтеся успадковувати будь-який клас в ієрархії Qt3DCore::QSceneChange та підігнати його відповідно до ваших потреб. Загальний підхід полягає в успадкування Qt3DCore::QStaticPropertyUpdatedChangeBase , який обробляє властивість ім'я та підклас додає строго типізований член класу для якості значення корисного навантаження. Перевага цього перед вбудованим механізмом у тому, що він уникає використання QVariant , який трохи страждає від високопоточних контекстів з погляду продуктивності. Як правило, характеристики фронтенд не змінюються дуже часто, і за умовчанням це нормально.
Висновок
У цій статті ми показали, як реалізувати більшу частину бекенд-вузла; як реєструвати співставник вузлів з метою створення, пошуку та знищення бекенд-вузлів; як безпечно ініціалізувати бекенд-вузол з фронтенд-вузла, а також як синхронізувати його дані з фронтендом.
У наступній статті ми, нарешті, зробимо наш аспект користувача, дійсно виконаємо якусь реальну роботу і дізнаємося, як змусити бекенд-вузол відправляти оновлення на фронтенд-вузол (середнє значення fps). Ми забезпечимо виконання важких частин у контексті пулу потоків Qt 3D, щоб ви зрозуміли, як він може масштабуватись. До скорого.
Стаття написана: Sean Harmer | Середа, Грудень 13, 2017р.