Чи траплялося вам грати в гру, де можна вільно досліджувати світ, і раптом ви потрапляєте на екран завантаження? Особисто я вважаю екрани завантаження дещо набридливими, тому я вирішив дослідити, як великі ігри вирішують цю проблему. І саме тоді я натрапив на щось, що називається стрімінг рівнів.
Коротке введення
Я другокурсник у Університеті прикладних наук Брєди, навчаюсь за напрямом Creative Media & Game Technologies. У цьому блозі я розповім про стрімінг рівнів у іграх з відкритим світом, поясню, що це таке, розгляну деякі поширені техніки та поділюсь своєю реалізацією на C++.
Зображення стрімінгу рівня в дії.
Що таке стрімінг рівнів?
По суті, стрімінг рівнів — це техніка розробки ігор, яка динамічно завантажує та вивантажує частини ігрового світу в залежності від місця розташування та дій гравця. Замість того, щоб завантажувати весь світ одразу, що є ресурсномістким і непрактичним, стрімінг рівнів гарантує, що в пам'яті активні лише необхідні активи. Це включає текстури, моделі, анімації та навіть ігрову логіку.
Щоб впровадити стрімінг рівнів у вашій розробці гри, є кілька технік, вартих уваги, таких як стрімінг на основі відстані, тригери об'ємів, асинхронне завантаження, відсічення фрустума, рівень деталізації (LOD), завантаження чанків та просторова сегментація.
Асинхронне завантаження
Ця техніка є однією з найкращих для впровадження стрімінгу рівнів у вашу гру. Причина в тому, що завантаження великої кількості даних одразу може спричинити значні лаги в грі. Щоб цього уникнути, процес завантаження обробляється в окремому потоці, а головний потік виділяється лише для рендерингу. Ось базова логіка:
function load_assets_async(asset_list):
for each asset in asset_list:
await load_asset(asset)
Це логіка, яку я використав у своїй реалізації. Оскільки в моєму рушії вже був інтегрований пул потоків, я вирішив його використовувати. Кожного разу, коли мені потрібно було завантажити кілька моделей в мою гру, я використовував tiny_gltf для завантаження моделі та її текстури за допомогою stbi_image в окремому потоці з пулу потоків. Тільки після завершення завантаження я додавав модель на екран.
Для відстеження всіх завдань, призначених окремим потокам, я використовував std::vector
об'єктів std::future
, що дозволяло мені отримувати результати, які вони повертають. Я створив просту функцію для перевірки, чи завершено завдання в майбутньому, і як тільки воно завершувалось, я додавав модель на екран, використовуючи отримані дані. Ось спрощена версія того, як я це реалізував у коді:
void StreamLevel::LoadMultipleModelThreadPool()
{
dispach.reserve(row * cols);
for (int i = 0; i < row*cols; i++)
{ //Помістити функцію завантаження моделі в інші потоки
std::future future = ThreadPool().Enqueue(LoadModel, filePathsModels[i].path);
dispach.emplace_back(ModelDispatch{std::move(future), i});
}
}
std::shared_ptr LoadModel(const std::string& path)
{ //Завантажити модель за допомогою бібліотеки tiny_gltf
Model model = LoadResource;
model->m_image_datas.reserve(model->GetImages().size());
for (int i = 0; i < static_cast(model->GetImages().size()); i++)
{
//Завантажити текстуру за допомогою бібліотеки stbi_image
// та зберегти дані зображення в моделі.
model->m_image_datas.emplace_back(image_data);
}
return model;
}
void StreamLevel::Update()
{
if (!dispach.empty())
for (int i = 0; i < dispach.size(); ++i)
{ //Перевірка кожного future з dispach
if (dispach[i].future.valid())
if (ThreadPool::IsReady(dispach[i].future))
auto data = dispach[i].future.get();
if (!data) continue;
// Якщо готово, додаємо модель на сцену
AddToScene(data, dispach[i].modelIndex);
}
}
Стрімінг на основі відстані та відсічення фрустума
Наступним кроком у впровадженні стрімінгу рівнів у вашому проєкті є визначення, які об'єкти в світі повинні бути завантажені. Це включає перевірку основних умов: чи знаходяться моделі на певній відстані від гравця? Чи знаходяться вони в полі зору гравця? Ці перевірки є основою логіки ефективного стрімінгу рівнів.
function distance_stream(asset_list):
for each asset in asset_list:
if is_in_distance(player.position, radius)
if is_in_frustum(asset)
true: stream_asset(asset)
else
false: unload_asset(asset)
Кол circle представляє зону, що транслюється. Трикутник — це відсічення фрустума. Разом вони транслюють лише моделі, підсвічені зеленим.
Як показано на зображенні, логіка проста: якщо об'єкт знаходиться в відстані камери та в полі зору, завантажуємо його. Для обчислення відстані між камерою та моделями я використав функцію glm::distance
, яка обчислює відстань між двома точками у світі. Якщо результат перевищує радіус, об'єкт вивантажується з пам'яті.
Для відсічення фрустума я реалізував метод з Learn OpenGL (ПОСИЛАННЯ). Кожного разу, коли відсічення фрустума активується, він оновлюється на кожному кадрі та перевіряє, чи знаходяться моделі в фрустумі. Якщо вони не в ньому, об'єкти вивантажуються з гри.
Показано, як працюють стрімінг на основі відстані та відсічення фрустума (в правому куті — міні-мапа з висоти).
void StreamLevel::Update()
{
//...
if (isStreaming)
{
if (isFrustrum)
{
auto view_matrix = GetViewMatrix();// Матриця виду камери
culling::UpdateFrustum(frustum, view_matrix, Camera.position);
}
for (int i = 0; i < row * cols; ++i)
{
auto& modelData = filePathsModels[i];
if (isFrustrum && !IsInFrustum(frustum, modelData.collision))
{ //Якщо модель не в фрустумі, вивантажуємо її
TileDestroy(i);
modelData.stream = false;
continue;
}
if (glm::distance(modelData.position, Camera.position)) > radius)
{ //Якщо модель не в радіусі, вивантажуємо її
TileDestroy(i);
modelData.stream = false;
}
else if (!modelData.stream)
{ //Якщо модель в радіусі та фрустумі — завантажуємо її
auto future = ThreadPool().Enqueue(LoadModel, modelData.path);
dispach.emplace_back(ModelDispatch{std::move(future), i});
modelData.stream = true;
}
}
}
//...
}
Додаткові методи для досягнення стрімінгу рівнів
Наступні техніки не були реалізовані в моїй грі, але я вважаю, що вони могли б стати чудовими доповненнями для покращення мого проєкту. Я коротко їх поясню.
Рівень деталізації (LOD) — це техніка, яка регулює складність 3D-моделей залежно від їх відстані від камери. Об'єкти, що знаходяться далеко від гравця, можна рендерити з меншою деталізацією, що економить потужність процесора. Наприклад, Fortnite і Apex Legends використовують систему LOD для збільшення FPS і створення захоплюючого ігрового процесу. Ви можете помітити це, коли використовуєте снайпер, адже моделі стають менш деталізованими.
Мапа рівнів деталізації: жовтий — найбільша деталізація, білий — найменша.
(Кредит — https://developers.meta.com/horizon/documentation/unity/po-assetstreaming/)_
Тригери об'ємів (Trigger Volumes): Ця техніка завантажує регіон щоразу, коли відбувається певна межа, подія, взаємодія або навіть ігрова механіка. Тому ці тригери завантажують та вивантажують певні ділянки карти, які ви будете досліджувати, проходячи через катсцену, відкриваючи двері або просто досягнувши точки спавну. Ви можете вручну вибирати, що завантажувати, коли хочете використовувати цей метод, але це може стати проблемою, якщо завантажити забагато. Коли ви проходите через великий об'єкт у Uncharted, є катсцена, яка приховує завантаження. Подібним чином, коли ви виходите з будинку в Assassin’s Creed, ви можете побачити сплеск лагів, що є завантаженням численних моделей.
Завантаження на основі чанків (Chunk-Based Loading), це проста методика для групування активів в сітку. Як тільки ви визначите, до якого чану з сітки належить об'єкт, його стане легше транслювати. Найкращий приклад цього — Minecraft. Спочатку генерується карта, а потім завантажується лише заданий радіус чанків, залежно від вибору гравця.
Просторова сегментація (Spatial partitioning) — це процес розбиття ігрового світу на менші, більш керовані частини. Це дозволяє ігровому рушію знати, які частини, в залежності від положення гравця, потрібно завантажити. Ви можете оптимізувати обчислення, сегментуючи вид або світ на керовані частини, замість того, щоб перевіряти кожен піксель.
Існують дві популярні техніки для просторова сегментації:
- Квадртрі (Quadtrees): Ця техніка рекурсивно розділяє площину на чотири частини. 2D і 3D ігри з плоскими середовищами отримують велику вигоду від цього.
- Октотрі (Octrees): Використовуючи цю техніку, куб рекурсивно ділиться на вісім частин. Вона добре працює в 3D середовищах.
Як розділяється квадртрі (Кредит : https://carlosupc.github.io/Spatial-Partitioning-Quadtree/)
Як розділяється октотрі. (Кредит : https://carlosupc.github.io/Spatial-Partitioning-Quadtree/)
Виклики при стрімінгу рівнів
Хоча стрімінг рівнів є потужним інструментом, він не позбавлений проблем:
- Поп-ін та затримки: Якщо активи не завантажуються вчасно, гравці можуть помітити, як об'єкти з'являються раптово. Це може порушити занурення в гру та розчарувати гравців.
- Складність налагодження: Чим більше ви просуваєтесь в стрімінгу рівнів, тим більше рівнів складності додається до вашого коду. Тому важливо включати багато інформації про те, що ваш код робить, щоб полегшити процес налагодження. Код стає набагато простішим і зручнішим для роботи з таким тестуванням. Крім того, ви гарантуєте, що активи завантажуються точно та ефективно, не спричиняючи проблем з продуктивністю.
В КІНЦІ
Стрімінг рівнів — це важлива техніка для розробки захоплюючих, величезних ігор з відкритим світом. Дозволяючи розробникам правильно керувати ресурсами та створювати безшовний досвід, ми покращуємо ігровий процес в іграх з відкритим світом.
Світ гри постійно розширюється і стає все складнішим, що робить ефективний стрімінг рівнів критично важливим для будь-якої гри.
Джерела
[
Алгоритмічні техніки для динамічного стрімінгу рівнів у розробці ігор
Динамічний стрімінг рівнів є важливою складовою сучасної розробки ігор, що дозволяє розробникам створювати масштабні світи…
peerdh.com
](https://peerdh.com/blogs/programming-insights/algorithmic-techniques-for-dynamic-level-streaming-in-game-development?source=post_page-----0afdd8ffed88--------------------------------)
[
Просторова сегментація-квадртрі
Оптимізація просторового упорядкування: Квадртрі
carlosupc.github.io
](https://carlosupc.github.io/Spatial-Partitioning-Quadtree/?source=post_page-----0afdd8ffed88--------------------------------)
[
Meta Developers
Редагувати опис
developers.meta.com
](https://developers.meta.com/horizon/documentation/unity/po-assetstreaming/?source=post_page-----0afdd8ffed88--------------------------------)
[
Обробка асинхронних операцій
Кілька методів з Addressables API повертають структуру AsyncOperationHandle. Основне призначення цієї структури — це…
docs.unity3d.com
](https://docs.unity3d.com/Packages/[email protected]/manual/AddressableAssetsAsyncOperationHandle.html?source=post_page-----0afdd8ffed88--------------------------------)
https://toxigon.com/unreal-engine-level-streaming-guide
Перекладено з: Level Streaming in Open-World Games: Revolutionizing Immersive Experiences