Введение
В основном эта статья будет полезна начинающим разработчикам, у которых уже есть небольшой опыт работы с движком (может пару месяцев или пол года). Если вы считаете себя мидл девелопером - то думаю, что вы тоже найдете для себя что-то полезное и нужное. Уверен, что и артистам, работающим с Unreal Engine, данная статья может быть полезна.
Стоит заметить, что если вы уже опытный синьор помидор - и этап первоначального накопления опыта вы давно прошли - для вас тут будет мало чего применимо на практике, а некоторые советы - могут быть вредны, например в части “Основа в С++”.
Эта статья - не туториал или гайд “как делать правильно”, я не даю каких то четких последовательностей действий, что бы добиться наилучшего результата. Я стараюсь поверхностно пройтись по, так как мне кажется, наиболее важным для новичков аспектам разработки на Unreal Engine. Моя цель - подсказать вам направления, в которых можно и нужно копнуть поглубже. Рассказать что есть и для чего это можно использовать. Почти для всех пунктов я привожу ссылки на более конкретные гайды и описательные статьи по темам. Они могут вам помочь.
Статья получилась весьма большая. Сначала это был маленький список тем, буквально 2-3 страницы, которые я активно пропагандировал несколько лет назад в компании, в которой я работаю. Тогда и сейчас я много времени уделял наставничеству начинающих разработчиков и вместе с этим сам многому научился. Недавно я случайно откопал эту заметку, решил раскачать ее в полноценную статью и поделиться ею с сообществом! Надеюсь что описанные мною вещи будут вам интересны и полезны. Желаю приятного чтения :-)
Общие архитектурные советы
Основа в С++
💡 Скорее всего со временем вы почти забудете что Blueprint существует.
Если вы делаете игру на Unreal Engine которую планируете поддерживать после релиза или в целом планируете долгую разработку (пол года и более), скорее всего вам потребуется писать на С++, как бы вам этого не хотелось.
Если вы еще маленькая/молодая/не опытная команда скорее всего вы подумаете, о том, что блюпринты вас спасут. Да, это действительно мощный инструмент, на котором можно делать много чего, но заменить С++ в полной мере он вам не позволит. В какой то момент вы просто станете смотреть на лапшу прилипшую ко дну кастрюли и плакать.
Чтобы не закладывать себе бомбу замедленного действия и не тратить потому кучу времени на рефакторинг, не хвататься за голову, когда какой-то маленький кусочек функционала придется реализовать в С++ (например в целях оптимизации), следует придерживаться нескольких простых правил:
-
У любого относительно большого/важного объекта должен быть С++ родитель
-
Все поля за исключением временных должны быть объявлены в С++ и вынесены в блюпринты
-
Большее число функций ваших объектов должны быть implementable или native событиями С++. Реализацию же, вы можете делать на блюпринтах
-
Типы, такие как перечисления или структуры должны быть объявлены в С++ и вынесены в блюпринт как
BlueprintType
Это весьма просто и под силу даже самому зеленому джуну, главное верить в себя! В таком случае у вас не будет барьера что бы писать С++ тогда, когда это потребуется. Вам не нужно будет переносить основу ваших объектов и заниматься этим несколько часов (а может и дней) к ряду, вам просто нужно будет переносить отдельные куски логики, что займет совсем немного времени и моральных ресурсов.
Когда в вашем проекте будут появляться длинные макароны, более 20 блоков можно задуматься о переносе их в С++, ведь теперь это не так сложно: вы знаете как оно работает, вы это уже отладили и у вас получится переписать это так, чтобы оно работало почти сразу. Особенно стоит обратить внимание на ту логику, которую вы в будущем планируете расширять, было бы здраво переписать то, что есть сейчас на С++ с учетом точек расширения, которые вы можете вынести в блюпринты, следуя советам указанным выше. Таким образом, у вас уже будет место для будущих улучшений.
Но есть и вещи которые делать в макаронах можно и в какой то степени нужно, в целях ускорения разработки:
-
Работу с эффектами/материалами, анимациями, одним словом - визуал и звуки
-
Таймлайны, по сути тоже, часто, можно отнести к визуалу
-
UI, но так же стоит учитывать о наличие C++ родителя у виджетов
-
Последовательности событий с делеями, аля заскриптованные моменты
Важный момент про UI! Дизайнить UMG виджеты в С++ вы не сможете, а делать игровой UI на Slate та еще плохая затея. Но не нужно унывать, есть замечательный мета-модификатор BindWidget
. Если вы объявите переменную компонента (сабвиджета) вашего виджета с этим модификатором и в БП наследнике создадите элемент с таким же именем - то он будет положен в эту переменную и вы сможете легко и просто использовать ее в С++, без какого либо дополнительного кода по получению компонентов и т.п. Тут можно почитать про BindWidget.
Следуйте хоть каким то паттернам
Паттернов очень много, они предназначены для решения шаблонных задач, которых в программировании, на самом деле очень много, хоть мы все и говорим, что программирование - это творческое занятие! Зная о паттернах и следуя им вам будет проще понимать код, который написал с использованием паттернов, и ваш код будет более логичным, понятным и объяснимым. Вам не придется понимать, почему вы сделали именно так, а не иначе (на самом деле конечно придется, но в меньшей степени).
Так же следование паттернам может сберечь вас от принятия плохих архитектурных решений, созданий некоторых велосипедов и выстрелов себе в ногу.
Про то какие паттерны существуют и как их использовать можно почитать тут.
Используйте и изучайте готовые решения
Нет, я не призываю вас под каждую задачу тянуть библиотеку, как по слухам это любят делать в веб разработке. Однако же, когда перед вами стоит какая то задача стоит понимать, что скорее всего эту задачу или похожую кто то уже делал, это может быть разработчик движка (тогда ваше решение у вас под ногами), ваш коллега, или рандомный бородатый мужик живущий на маленьком острове посреди пролива Ла-Манш.
Поищите нужное вам решение внутри движка (там очень много всего, вы даже не представляете на сколько), для этого вы можете почитать код или документацию, погуглите, почитайте форумы, статьи, поройтесь в публичных репозиториях и скорее всего вы найдете что ищите. Заходить нужно на столько далеко, на сколько это оправдывает важность и сложность задачи, точно не стоит останавливаться на третей ссылке в гугле, но и не нужно каждый раз сидеть в дебрях сети днями или неделями.
Хочу заметить, что с найденной информацией тоже нужно правильно обращаться. Возможно что-то вы сможете просто использовать, как есть. Что то вам понадобиться модифицировать под себя. А чем то просто вдохновиться и сделать свое решение.
Соблюдайте цикл жизни ваших сущностей
Цикл жизни каждого объекта должен начинаться с корректной инициализации, а заканчиваться остановкой его работы и удалением. Такой подход упростит вам написание кода и поможет избежать того, что мы называем undefined behaviour (это когда хрень какая то происходит).
Инициализация
На этапе инициализации нужно подготовить ваш объект к дальнейшей работе и взаимодействию с другими объектами.
Если объект использует какие то вещи, которые объекту нужно создать или получить из внешней среды - это нужно учесть во время инициализации. Чтобы в коде основной логики объекта не мешался поиск какого то актера на уровне, а просто использовались уже проинициализированные поля. Архитектура Unreal уже имеет функции у многих базовых классов, которые используются ими для инициализации и их можно переопределить:
-
все UObject -
PostInitProperties
,PostLoad
-
AActor -
PostInitializeComponents
,OnConstruction
, (и другие) -
UActorComponent -
OnRegister
,InitializeComponent
-
UUserWidget -
OnNativeInitialized
,OnNativeConstruction
-
Старайтесь не использовать
BeginPlay
для инициализации - вместо него есть много всего. Особенно в мультиплеерных играх, если используешьBeginPlay
- всегда нужно делать в голове, что скорее всего он вызовется только на сервере.
Однако, зачастую для инициализации пользовательских объектов, требуются передать какие то данные, в таком случае правильнее сделать свою функцию инициализации, которая эти данные примет и проинициализирует объект.
Правильное удаление
Если вы более не планируете использовать какую то сущность - завершите ее работу и удалите. Не стоит оставлять то, что вам более не потребуется, оно может вам помешать.
Важно не только удалиться но и сделать это правильно: скинуть таймеры, разбиндить делегаты, удалить какие нибудь зависимые объекты, оповестить кого надо. Unreal Engine многое из этого сделает за вас благодаря GC и рефлексии, но возможно что то у него не получится, по этому не стоит пренебрегать четким заверением работы объекта.
Функции подходящие для завершения работы:
-
все UObject -
BeginDestroy
,FinishDestroy
-
AActor -
EndPlay
,Destroyed
-
UActorComponent -
OnUnregister
,OnComponentDestroyed
-
UUserWidget -
NativeDestruct
Но вы так же можете делать и свои функции которые корректно будут завершать работу в вашей ситуации.
Используйте базовые классы из GameFramework (и не только) правильно
Делай так, как предлагает анриал, наследуйтесь от одного из этих классов для выполнения ваших задач и вам будет классно:
-
GameMode - подключение игроков, стадии игры и условия победы
-
GameState - статистика/управление состоянием игры, возможно управление событиями происходящими в игре
-
PlayerState - состояние игрока
-
Pawn - сущность в игровом мире, которой может управлять игрок или ИИ. Подходит для персонажей игрока
-
Character - Pawn гуманоидного вида. Если не вдаваться в детали отличается от Pawn только тем, что отлично подходит для реализации гуманоидных персонажей управляемых игроком или ИИ.
-
PlayerController - неизменная сущность игрока, одной из первых получает ввод, характеризует конкретный клиент.
-
MovementComponent - логика перемещения, если не подходит существующие компоненты движения, написать свой на базе этого класса будет лучшим решением
-
ActorComponent - если вы хотите дать своим актерам какое то новое свойство или функционал, хорошим вариантом может стать инкапсуляция этой логики в компоненте.
-
<Any>Subsystem - ваша кастомная логика, у которой может быть свое состояние, отсутствует потребность в репликации и которая не является объектом в игровом мире.
-
AnimInstance - управление анимациями, если ваша работа с анимациями комплекснее чем написание машины состояний и простенький анимационный блюпринт, например ,предполагает какие то сложные вычисления - вы можете воспользоваться классом анимационного инстанса. Это родитель всех анимационных принтов.
Используйте сабсистемы
Когда вам нужно написать, какой то код, который как вам кажется не должен иметь оболочку в мире игры - советую выбрать какой то из сабсистемов, для реализации подобной задачи.
Какие у этого решения плюсы:
-
Сабсистемы создаются и удаляются сами, не нужно над этим заморачиваться
-
Инициализация вместе с тем, к чему относится (Editor, Engine, World, LocalUser, GameInstance)
-
Весьма удобная доступность из любой части кода игры (в том числе из блюпринтов)
-
Возможность инкапсулировать в вашей сабсистеме какую то часть логики, а не писать ее в класс где уже есть другая логика
https://docs.unrealengine.com/4.27/en-US/ProgrammingAndScripting/Subsystems/ - тут можно почитать про них страницу в документации. В документации нету UWorldSubsystem
но она существует и в ней можно делать вещи, которые относятся к миру игры.
Не парсите JSON’ы руками
На самом деле это очень узкая штука, ведь в каких то проектах мы вообще не встречаемся с JSON, но все же на моей практике были моменты когда на ревью приходил код в несколько десятков строк с парсом json’ов.
JSON желательно собирать и парсить при помощи структур и конвертации (FJsonObjectConverter) json в структуру и обратно. По структуре очень просто можно понять что хранится в json, не прибегая к прочтению кода парсинга. А так же это очень быстро, просто и эффективно.
Парсить жсоны руками - плохо, почему:
-
Нужно писать какой то свой код для парса (время, возможно будут ошибки)
-
Не гибко, нужно масштабировать так же руками с обоих сторон
Если что-то плохо конвертируется в Json, например если в структуре есть TOptional
или иные поля не поддерживающие рефлексию можно использовать ImportTextItem
или ExportTextItem
, это грубо говоря парсинг, написанный руками, но в некоторые моменты приходится писать свою логику для таких базовых вещей как парс JSON’ов.
Взаимодействие
Под взаимодействием объектов в рамках игры мы обычно понимаем вызов функций из одного объекта в другом. Или вызовы из одной группы объектов в другую (например разные игровые системы).
Для контроля взаимодействия наших объектов и систем есть два хороших решения:
Используйте события и делегаты
Скорее всего вам потребуется много событий, почти на все. Так что если вы заранее будете создавать события/делегаты в тех местах где что-то происходит - скорее всего вы поможете себе в будущем, когда это событие вам потребуется. Ведь, если его не будет - нужно будет идти его добавлять, а это может быть неудобно и багоопасно.
💡 Возможно, в будущем я напишу статью о том, как можно весьма просто и удобно пользоваться делегатами и событиями в Unreal и брать от них все.
На мой взгляд самый важный принцип парадигмы ООП, который по сути не реализован в С++ и многих Си-подобных языках - это обмен сообщениями. Такой подход наиболее хорошо раскрыт в языке Smalltalk, однако этот язык не стал популярным.
При помощи событий вы сможете построить (весьма безопасную) систему обмена сообщениями, в которой объект, который хочет послать сообщение, будет вызывать событие, а объекты подписанные на событие - будут являться принимающей стороной. Такая система удобна тем, что позволит быстро расширять ваши взаимодействия, и достаточно просто отслеживать неполадки в взаимодействие ваших сущностей.
Используйте интерфейсы (программные)
Программные интерфейсы очень полезны для инкапсуляции функционала, которая в свою очередь способствует модульности вашего решения и позволяет проще вносить изменения в конкретный модуль (или его часть), сокрытый под интерфейсом.
Проектировать интерфейс лучше исходя из потребностей модуля который к нему будет обращаться, а не исходя из возможностей модуля который его реализует. Такой подход позволяет уменьшит blast radius.
Blast radius - это понятие, описывающие ущерб в виде багов или неработоспособности системы вызванный внедрением нового функционала или переработкой старого.
Зачастую при изменении модуля радиусом поражения можно считать все модули, которые с ним напрямую связанны. Иными словами чем четче и конкретнее мы делаем связи модулей - тем меньше мы страдаем от внедрения новых фич. Управлять связанностью сущностей, на мой взгляд, проще всего используя интерфейсы. Естественно, помимо делегатов и событий.
Один из принципов SOLID, а именно принцип разделения интерфейсов (I) гласит:
“Создавайте узкоспециализированные интерфейсы, предназначенные для конкретного клиента. Клиенты не должны зависеть от интерфейсов, которые они не используют.”
Под клиентом тут понимается сущность, которая использует интерфейс, а не реализует его.
Подробнее про принципы SOLID можно почитать в этой статье. Или в цикле видео на канале Сергея Немчинского.
Оптимизации
Если вы делаете не большую игру и уже пользуетесь всеми или большинством советов из этой статьи - то скорее всего она у вас не сильно лагает. Однако оптимизировать все равно скорее всего придется. На первом этапе вам потребуется понять что вообще в вашей игре вызывает лаги и фризы, для этого в Unreal Engine (и не только) полно инструментов.
Однако, перед тем как переходить коду или ассетам есть несколько простых вариантов, которые вам могут помочь если дело не так плохо - настройки проекта. Вряд ли конечно оно решит вашу проблему, но посетить их стоит.
В настройках проекта (Вкладка Project Settings
) есть много всего, что может влиять на производительность. В основном оно живет в группе Engine
или в плагинах, если вы их используйте. Посмотрите на ваши настройки, погуглите что они значат, вдруг после изменения какой то из них ситуация станет лучше. Можно копнуть глубже и погрузиться в конфиги, так тоже много чего интересного и полезного, не только для оптимизаций.
Важное замечание! Оптимизировать вещи нужно тогда, когда это правда необходимо. При оптимизации нужно соблюдать баланс производительности и затраченных на это сил. Будет лучше если вы потратите на оптимизацию игры перед релизом несколько месяцев и выпустите проект не идеально оптимизированным, чем если вы потратите на это несколько лет, за которые ваш проект морально устареет, а компания разорится.
Как узнать что лагает
Самое важное на мой взгляд в процессе оптимизации - понять что вызывает проблемы. И начинать чинить с самых тяжелых мест. Для поиска проблем у вас, на самом деле очень, много вариантов!
Есть инструменты многие прям внутри движка, позволяющие оценивать производительность в рантайме, есть сторонний софт:
-
Разные режимы просмотра тяжести сцены в Optimization View Modes
-
Возможность смотреть во вьюпорте статистику по производительности которую собирает движок. Вы можете так же добавлять свои собственные счетчики
-
Профилировать CPU и GPU при помощи Session Frontend
-
Профилировать почти все при помощи Unreal Insights
-
Профилировать GPU разными встроенными в движок методами
-
Профилировать GPU при помощи сторонних программ, например RenderDoc или **Intel GPA (и тут еще)
-
Сеть можно профилировать как при помощи инструментов движка (лучше этим)так и при помощи снифферов смотреть че там вообще в сети плавает
Тик
Перейдем же к тику! Тик - это код, который вызывается каждый кадр, в нем происходит обновление игрового мира - обычно самое слабое по производительности и самое важное по сути место в игре. Так как любая видео игра строится на постоянном обновлении картинки.
Если говорить о тике - самые важные показатели: среднее и пиковое время обработки кадра. Среднее - это общая производительность, тот самый FPS, а пиковая - это фризы/лаги.
В этом разделе под тиком я в первую очередь понимаю логику, которая выполняется с основном потоке игры в процессе обновления мира. Но стоит помнить, что вся графика тоже рисуется каждый кадр.
Полностью избавиться от тика мы не можем, однако мы можем его разгрузить, тем самым сократив среднее и пиковое время рендера кадра.
Самые распространенные оптимизации тика, которые могут вам помочь:
-
Тикать реже (не каждый кадр) если это возможно, а в какие то моменты можно не тикать вовсе
-
Если вы в тике проверяете произошло ли какое то событие - переведите это на делегат
-
Обычно долго отрабатывают функции типа
GetAllActorsOfClass
(которые подразумевают обработку большого массива) возможно у вас получится искать всех нужных актеров в момент инициализации и использовать уже найденных -
Перепишите тик на С++, если на блюпринтах он выглядит красиво и не лагучо, но все равно долгий
-
Возможно некоторые вещи у вас получится сделать асинхронно, например обновлять множество объектов на за один кадр а по кускам за несколько кадров.
-
Можно зайти дальше и вытащить что-то в другой поток. В С++ например вы можете очень просто запускать асинхронный код в другом потоке при помощи функции
AsyncTask
. Если вам нужно обработать большой массив данных вы можете прибегнуть к использованиюParallelFor
это функция доступная в С++, которая обработает ваш массив в разных потоках и передаст управление вызывающему, когда закончит, для больших массивов это быстрее, чем делать в одном потоке. Тут вы можете найти примеры использования AsyncTask и ParallelFor -
Может быть что-то может не тикать в принципе, проверьте свои тики на бесполезный код, возможно вы что-то когда то давно забыли убрать
Про тик можно писать очень долго, но это становится нужно, скажу немного про хардкорные оптимизации и пойдем дальше.
Лучше всего если одинаковые функции будут вызываться друг за другом. Например если у вас есть большая группа одинаковых тикающих объектов - им стоит тикать вместе. Это обуславливается тем, что в таком случае процессору не нужно менять код функций в кэше инструкций и возможно у них есть некоторые общие данные, которые так же нет смысла загружать и выгружать кучу раз. На эту тему есть очень классное видео:
https://www.youtube.com/watch?v=CBP5bpwkO54
Зависимости и загрузка
Помимо тика еще одно узкое место - загрузка контента. Что бы избегать лагов на загрузке чего либо следует правильно строить зависимости ваших ассетов друг от друга. Связанность ассетов влияет на то что и когда загружается. Связи могут быть как жесткие, так и мягкие.
Жесткие связи могут вызывать две проблемы: лаги и долгие загрузки.
Все поля для ассетов, которые вы указываете как UMyAssetType* FieldName;
являются жесткими. И при загрузке сущности хранящей в этом поле ссылку на тот или иной ассет произойдет так же загрузка указанного ассета, даже если он в данный момент не нужен.
Может доходить до того, что при открытии любого уровня движок грузит в ОЗУ всю игру, из-за чего, казалось бы, маленький уровень загружается 10-ки минут. А лаги у вас могут быть когда вы пытаетесь что либо заспавнить, например оружие которое может наносить урон противникам, в таком случае оно потянет за собой загрузку противников и все их зависимости. Все это будет происходить в рамках одного кадра, что сильно растянет его время и вызывает лаг/фриз.
💡 Давным давно я сам так делал и люто от этого страдал.
У этих 2-х проблем есть ультимативное решение! Используйте мягкие ссылки везде где это можно!
Для определения мягкой (не слабой) ссылки в UE есть специальные типы:
-
TSoftObjectPtr<T>
- для определения типизированной ссылки на объект -
TSoftClassPtr<T>-
для определения типизированной ссылки на класс, аналогTSubclassOf<T>
-
FSoftObjectPath
иFSoftObjectClass
- нетипизированные мягкие ссылки для объектов и классов соответственно, однока мы можете их типизировать при помощи мета модификаторов:-
AllowedClasses
для объектов -
MetaClass
иMustImplement
для классов (второй существует но по какой то причине не указан в документации)
-
Однако при использовании мягких ссылок придется загружать ресурсы, на которые они указывают самостоятельно, это не сложно и можно делать как синхронно (на текущем кадре), так и асинхронно (за несколько кадров).
Скорее всего, если вы будете использовать мягкие ссылки и загружать ресурсы синхронно в момент, когда они действительно нужны - это уже спасет вас от фризов и долгих загрузок. Если нет - можете думать в строну асинхронной загрузки.
Синхронная загрузка представляет из себя вызов одной функции:
-
TryLoad
для мягкий путей (SoftPath) -
LoadSynchronous
для мягких указателей (SoftPtr)
С асинхронной загрузкой все сложнее и есть хорошие материалы на эту тему. Подробнее про асинхронную загрузку можно почитать тут и тут.
Сборщик мусора
Сборка мусора часто вызывает фризы, по этому эпики придумали несколько вариантов того, как вы можете ее для себя оптимизировать. Например, ее можно вызывать вручную, в те моменты когда у игрока темный экран, или когда происходит какая то загрузка/интро. Можно так же при возрождении игрока, обычно оно проходит через фейд.
Сборка мусора так же всегда вызывается на загрузках уровней.
Можно включать кластеры GC, что бы GC проводил поиск мусора быстрее.
Если мусора много - можно вызывать GC чаще, в таком случае он будет удалять меньше объектов.
Про сборку мусора я писал отдельную статью, советую ее почитать, говорят хорошая.
Скелетные меши
По сути оптимизация скелетный мешей - это тоже оптимизация тика, но весьма конкретного и тяжелого.
Вы можете использовать настройки на вкладке Details/Optimization
, там их много, посмотрите на них, они реально работают. Например там можно не тикать когда игрок не видит или не проверять оверлапы при воспроизведении анимаций.
Еще вы можете следить за тем, что бы одинаковые склетал меши тикали вместе, так как их тики очень большие и глубокие. (Возвращаю вас к видосику про оптимизации Sea of thieves)
А еще вы можете их инстансить (ну почти)! Это прикольно, попробуйте.
Лоды
Во первых, если у вас есть меши - у них должны быть лоды. При чем нулевой лод должен быть видет в те моменты, когда вам действительно нужна самая лучшая детализация. Зачастую даже на переднем плане хорошо себя показывает и первый лод. Лодировать можно как статические сетки так и скелетные.
Проблема которую решают лоды не тяжелая работа вершинного шейдера, а перерисовка, то есть обновление одних и тех же пикселей при отрисовки полигона. Эту проблему решают и другие механизмы, но лоды играют немаловажную роль в этом.
Качественные материалы с полным сетом PBR текстур - весьма дорогие. На 0-1-2 лодах следует так же менять материалы на более легкие.
С большой долей вероятности на далеких объектах можно порезать использование каких либо текстур (например шершавости), и заменить на скалярный параметр. На супер далеких лодах можно просто использовать солидные цвета если это не заметно.
Важно! Если вы используете Nanite мои советы для вас могут быть не сильно релевантны.
Батчинг
Вот это вот непонятное слово означает процесс, при котором много объектов превращаются в один. И тем самым обрабатываются быстрее. Стоит отметить, что батчинг работает только со статичными стеками, однако некоторые умельцы могут батчить и скелетные сетки, но про это не в этой статье и по дефолту движок так не умеет.
В анриале есть 2 инструмента которые могут в это: Actor Merging и HLOD.
Actor Merging - это инструмент движка, который позволяет вам вручную запекать ваши меши и актеры в один меш, материалы и текстуры он так же может запекать.
HLOD. Если прошлый инструмент подразумевает ручную работу, то HLOD уже работает почти автоматически в процессе игры вам его нужно лишь настроить. Он использует смерженные прокси мешы в процессе игры и оставляет вам возможность редактировать ваши меши по одиночке, в процессе левел дизайна. Однако эта штука очень любит кушать оперативу, стоит быть аккуратным.
Стриминг уровней
Основная проблема в стриминге может быть в том, что ваши уровни долго грузятся или долги инициализируются (в это время там спавнятся актеры и регистрируются компоненты).
Следовательно у вас есть 2 пути оптимизации:
-
Делать уровни поменьше, что бы они загружались побыстрее. Если уровень загружается дольше 5-ти секунд - это можно считать большим уровнем
-
Сокращать время спавна актеров и регистрации компонентов, для этого вы можете пооптимизировать ваши функции инициализации в этих актерах и компонентах или настроить то в каком объеме это может происходить за кадр в настройках проекта в разделе стриминга
Никогда не нужно держать все уровни загруженными в память постоянно. Загружены должны быть только уровни которые видно сейчас и которые игрок увидит в ближайшие 10 секунд.
Если нужно показать уровень который находится далеко - можно сделать для него дополнительный облегченный вариант и показывать его издалека, а вблизи уже грузить основной.
Важное замечание! В эдиторе стриминг уровней всегда блокирующий - то есть он загружает весь уровень за один кадр. Если вы будете проверять оптимизацию стриминга в эдиторе - скорее всего он у вас будет лагать всегда.
И еще одно! Это относится только в UE4, так как в UE5 это уже по другому работает.
Материалы и текстуры
Оптимизация текстур, а особенно материалов - очень глубокая тема. Мы рассмотрим несколько ее аспектов:
-
Старайтесь использовать текстуры реально нужного вам разрешения, не нужно везде использовать 4к или 2к текстуры. Порой можно обойтись даже 256х256 текстурой, просто правильно ее использовать в материале.
-
Ваши текстуры должны быть со стороной в степени 2-ки. Желательно с соотношением сторон 1:1 или 1:2.
Это важно в первую очередь из-за совместимости. Квадратами в степени 2-х раньше выделяли память видеокарты (будь то десктопная или мобильная) и проводили все операции зная что текстура именно такая.
То есть если у вас текстура например 1000х100 то в памяти она будет занимать столько же сколько и 1024х1024. Сейчас многие десктопные видеокарты, начиная с 10-х годов нормально работают с текстурами не в степени 2-х. Но такая проблема все равно сохраняется на некоторых платформах нашего времени, преимущественно на мобильных.
Например, IOS не квадратные текстуры не в степени 2-х вообще не будет рендерить, вы получите просто белые пятна, вместо вашей текстуры. -
Старайтесь использовать материал инстансы вместо материалов. По большей части проблема в удобстве. Я отношусь к материалам следующим образом: материал - это шейдер, а материал инстанс - это материал. Тут вот есть про это ссылка. (раньше тут было другое, но мне объяснили где я не прав и я переобулся)
-
Следите за сложностью ваших шейдеров, чем он сложнее - тем дольше рисуется. Это можно делать при помощи режима просмотра Shader Complexity
-
Полупрозрачные материалы очень дорогие, старайтесь их использовать как можно меньше. Особенный ад начинается когда они накладываются друг на друга. Когда у вас есть полупрозрачный объект, вы должны нарисовать все: полупрозрачный объект, следующий за ним, иногда еще следующий, и только потом остальную часть сцены, это большая цена.
С непрозрачными объектам же, мы рисуем только видимые полигоны этого объекта и ты объекты сзади, которые он перекрывает не полностью.
Заключение
Как вы могли заметить я очень мало (почти ничего) сказал про сеть в Unreal Engine, это очень большая тема и я не придумал как можно раскрыть ее коротко, ведь статья правда получилась очень большой и без сети. Когда нибудь, я думаю что и про сеть статью напишу, а пока что могу предложить вам ознакомиться с штукой, которая называется Unreal Network Compendium, это достаточно старая, но до сих пор актуальная статья, которая, как мне кажется, полноценно описывает сеть в Unreal Engine.
Буду рад вашим отзывам и комментариям, если у вас есть какие то вопросы касательно тем, описанных в статье или вы с чем то не согласны - давайте обсудим!
Автор: Виктор Ковылин