Чистое и модульное решение с примером

|| Введение в систему

С выпуском UE5.1 в прошлом году Epic внедрила UMG ViewModel в качестве бета-плагина. Если вы поищите документацию — вы сильно разочаруетесь. О системе мало что говорят, но я думаю, поскольку она еще на ранней стадии, и мы подождем, пока Epic продвинет ее вперед.

А пока позвольте мне объяснить:

UMG ViewModel — это решение MVVM от Epic. Это шаблон проектирования, используемый для создания отдельного независимого пользовательского интерфейса путем удаления кода из пользовательского интерфейса и перемещения его в модель, с которой могут взаимодействовать как пользовательский интерфейс, так и функции взаимодействия с игроком (прыжки, удары руками, стрельба).

Мне нравится разбивать эти три на простые имена

Вид: Пользовательский интерфейс

Просмотр модели: Медиатор

Модель: Символ/Кодовая база

Его ценность для развития проявляется в том, что дизайнеры и художники могут повторять свои проекты, не касаясь кода. На любом этапе могут вмешаться художник и программист, и между ними ничего не будет конфликтовать. Итак, вы можете понять, почему это прибыльная идея для прототипирования и управления. (Новая игра TombRaider использует этот шаблон проектирования!)

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

Система управляема событиями. Это означает, что он может заменить существующие платформы, такие как BPI или диспетчеры событий.

Что касается недостатков, включение разделения может вызвать некоторые издержки производительности, если некоторые события не настроены должным образом, а также обычно требует больше памяти, поскольку ваша реализация может ввести больше переменных.

Теперь позвольте мне показать вам пример из моего собственного проекта (Primal Dominion: Aftermath), где эта система используется для эффективного обновления информации о магазине/боеприпасах.

Существующая система:

Примечание. Извините за скриншоты, если некоторые переменные могут не совпадать со скриншотами (некоторые функции и переменные были удалены и заменены статическими переменными по соображениям конфиденциальности).

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

На первый взгляд это кажется вполне разумным? Мы собираемся проверить, а затем вернуть текст, чтобы обновить текст…

Но ох… Мы привязали это к тексту…. Этот тип привязки не очень хорош, поскольку означает, что значение будет обновляться каждый кадр, в котором виджет виден.

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

Так как же мы можем это исправить?

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

Поэтому лучшее место, чтобы сообщить ViewModel об обновлении, — это после того, как вы уменьшили количество боеприпасов. Это также многопользовательская игра, поэтому мы можем получать обратные вызовы с сервера при обновлении переменной. Поскольку боеприпасы реплицируются, мы можем использовать функцию OnRep в репликации UE, чтобы получить этот обратный вызов (настоятельно рекомендуем вам изучить это, если вы новичок в репликации/сетевом коде). Если вы с ней не знакомы, это функция, которая срабатывает каждый раз, когда переменная обновляется сервером. Это значит, что мы можем использовать его как коррекцию сервера.

ПРИМЕЧАНИЕ. В реплицируемой среде очень важно обновлять данные либо с помощью ответов сервера, либо с помощью прогнозов клиента. Если вы используете переменные клиента, вы можете вызвать проблемы с синхронизацией, например, обновление боеприпасов происходит быстрее, чем сервер может успевать, что приводит к колебаниям значений. В следующих примерах кода я показываю только использование OnRep. Целесообразно использовать прогнозирование клиента, чтобы устранить задержку медленного обновления клиента.

На следующем снимке экрана у нас есть переменная, она использует OnRep, а определение функции возвращает делегат (или несколько, если вам нужны более точные обновления), к которому мы можем получить доступ в BP (его можно обменять на любое событие по вашему выбору). .

Затем мы привязываем это к событию, которое запускает RPC на клиенте для ViewModel, чтобы обновить его переменную значением реплицируемой.

Теперь, когда ViewModel обновлена, вся логика со стороны модели завершена, теперь ViewModel будет взаимодействовать с представлением (UI).

Когда эта переменная «Mag Ammo» обновляется, запускается событие «Установить» переменную:

Это другой тип сеттера, в нем есть некоторая логика, которая работает вместе с обновлением переменной.
На снимке экрана у нас есть оператор if, это потому, что макрос для обновления переменной вернет значение true, если новая значение отличается от старого.
Если он новый, запустите макрос BROADCAST_FILED_VALUE_CHANGED.
Трансляция делает именно то, что вы думаете: она отправляет сообщение, похожее на View (UI), чтобы сообщить ему, что все, что связано с функцией GetAmmoPoolString, должно быть обновлено.

Вот функция, которая привязывается, как вы можете видеть, она точно такая же, как функция виджета из неэффективной системы, за исключением того, что на этот раз у нас есть переменная в ModelView:

В выбранном нами представлении (UI) мы реализуем ViewModel, и, как вы можете видеть, доступна функция FString:

Затем мы можем выполнить специальную привязку к выбранному нами текстовому полю, чтобы текстовое поле можно было обновлять каждый раз при вызове функции:

Вот и все! Нет, правда, это все, что нужно для этого преобразования.

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

Я могу вернуться к этому посту в будущем с примером GitHub или видеоуроком по работе с шаблоном.