Как сделать пул объектов с асинхронной загрузкой ресурсов?

Программирование на Юнити.

Как сделать пул объектов с асинхронной загрузкой ресурсов?

Сообщение Инженер 13 июн 2020, 12:08

У меня есть рабочий пул, в котором объекты загружаются с помощью Resources.Load и передаются запрашивающему методу в одном вызове Update. Теперь хочу переделать под асинхронный вызов с помощью Resources.LoadAsync.

Не пойму, как в принципе может работать такой пул.

1) Вот что-то запрашивает объект, его нет в пуле, начинает загружаться новый ресурс. Проходит несколько кадров, ресурс загружен, инстанцируем объект из этого ресурса и передаем его в метод, который его запрашивал. Так, что ли?

2) Или может запрашивавший метод просто все время тыкается в пустой список пула каждый кадр, пока объект там не появится?

3) Как запомнить, какой метод какой объект вызывал и куда его надо вернуть после загрузки? Самая большая проблема для меня.

4) У меня статический пул, значит IEnumerator'ы я не могу использовать. Хотелось бы сохранить статичность, очень уж удобно вызывать спаун объекта из любого места. Значит, я должен использовать делегаты, предоставляемые ResourceRequest'ами, верно? Я при вызове делегатного метода, убираю его из делегата, чтобы он там больше не висел, правильно? Типа вот так:


Синтаксис:
Используется csharp
                static void GetNewEntity(ushort prefabID)
                {
                        resourceRequest = Resources.LoadAsync(folder + prefabID.ToString(), typeof(GameObject));
                        resourceRequest.completed += OnResourceLoaded;
                }

                static void OnResourceLoaded(AsyncOperation obj)
                {
                        obj.completed -= OnResourceLoaded;
                        GameObject newGO = GameObject.Instantiate(resourceRequest.asset as GameObject) as GameObject;
                        DefaultEntity result = newGO.GetComponent<DefaultEntity>();
                        Despawn(result);
                }


Ужас, что мне всю цепочку методов придется переделывать под асинхронность. Надо было сразу асинхронный пул писать. Подскажите хотя бы, как лучше организовать сам пул. Вот код СИНХРОННОГО пула:


Синтаксис:
Используется csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace WorldStreaming
{

        public static class PoolManager
        {
                private static Dictionary<ushort, Queue<DefaultEntity>> poolsDictionary = new Dictionary<ushort, Queue<DefaultEntity>>();
                private static string folder = "Streamed/";

                public static void Despawn(DefaultEntity target)
                {
                        if (poolsDictionary.ContainsKey(target.prefabID) == false)
                        {
                                CreateNewPool(target.prefabID);
                        }
                        poolsDictionary[target.prefabID].Enqueue(target);
                        target.Deactivate();
                }

                public static DefaultEntity Spawn(ushort prefabID)
                {
                        if (poolsDictionary.ContainsKey(prefabID) == false)
                        {
                                CreateNewPool(prefabID);
                        }

                        if (poolsDictionary[prefabID].Count > 0)
                        {
                                return GetEntityFromPool(prefabID);
                        }
                        else
                        {
                                return GetNewEntity(prefabID);
                        }
                }

                static void CreateNewPool(ushort prefabID)
                {
                        poolsDictionary[prefabID] = new Queue<DefaultEntity>();
                }

                static DefaultEntity GetEntityFromPool(ushort prefabID)
                {
                        DefaultEntity result = poolsDictionary[prefabID].Dequeue();
                        result.Activate();
                        return result;
                }
                static DefaultEntity GetNewEntity(ushort prefabID)
                {
                        GameObject newGO = GameObject.Instantiate(Resources.Load(folder + prefabID.ToString(), typeof(GameObject))) as GameObject;
                        DefaultEntity result = newGO.GetComponent<DefaultEntity>();
                        return result;
                }
        }
}
Инженер
UNIт
 
Сообщения: 88
Зарегистрирован: 22 май 2016, 11:13

Re: Как сделать пул объектов с асинхронной загрузкой ресурсов?

Сообщение 1max1 13 июн 2020, 13:00

Сделай синглтон и будут доступны корутины, там ты можешь спокойно дождаться isDone и инстанировать ресурс, без всяких подписок/отписок. Кстати, я не знаю кэширует ли юнька уже загруженные ресурсы, чтобы метод Load работал быстрее, возможно тебе придется позаботится и об этом тоже.
Аватара пользователя
1max1
Адепт
 
Сообщения: 5505
Зарегистрирован: 28 июн 2017, 10:51

Re: Как сделать пул объектов с асинхронной загрузкой ресурсов?

Сообщение Jarico 13 июн 2020, 20:52

1max1 писал(а):Кстати, я не знаю кэширует ли юнька уже загруженные ресурсы, чтобы метод Load работал быстрее, возможно тебе придется позаботится и об этом тоже.

Кэширует если загружается из Resources, с бандлами кэширует если только бандл выгружен с помощью Unload(false)
Github: _https://github.com/redheadgektor
Discord: Конь! Чаю!#9382 (сижу редко)
YouTube: _https://www.youtube.com/channel/UCPQ04Xpbbw2uGc1gsZtO3HQ
Telegram: _https://t.me/redheadgektor
Аватара пользователя
Jarico
Адепт
 
Сообщения: 1084
Зарегистрирован: 06 янв 2019, 17:37
Откуда: 0xDEAD
Skype: none
  • Сайт

Re: Как сделать пул объектов с асинхронной загрузкой ресурсов?

Сообщение Инженер 15 июн 2020, 10:04

1max1 писал(а):Сделай синглтон и будут доступны корутины

Нашел вариант лучше- More Effective Coroutines.

После дня экспериментов получается какой-то бред:

Синтаксис:
Используется csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MEC;

namespace Test
{
        public class CorutineMECasset : MonoBehaviour
        {
                private void Update()
                {
                        if (Input.GetKeyDown(KeyCode.Q))
                        {
                                Timing.RunCoroutine(SpawnEntityAsync());
                        }
                }

                IEnumerator<float> SpawnEntityAsync()
                {
                        GameObject go = new GameObject();

                        Timing.RunCoroutine(SpawnFromPool(go));

                        while (go == null)
                        {
                                yield return Timing.WaitForOneFrame;
                        }

                        go.name = "ОБЪЕКТ ГОТОВ " + Time.time.ToString();
                }

                IEnumerator<float> SpawnFromPool(GameObject go)
                {
                        yield return Timing.WaitForOneFrame;

                        //Здесь мы можем брать объект из пула, если пул содержит нужный объект
                        //Если нет, тогда спауним новый объект:

                        Timing.RunCoroutine(GetNew(go));
                }

                IEnumerator<float> GetNew(GameObject go)
                {
                        ResourceRequest rc = Resources.LoadAsync("OldMilitaryBed/Bed", typeof(GameObject));
                        while (rc.isDone == false)
                        {
                                yield return Timing.WaitForOneFrame;
                        }

                        go = Instantiate(rc.asset as GameObject) as GameObject;
                }
        }
}
 



Для спауна 1 объекта требуется запустить целых 3 корутины:

1 SpawnEntityAsync- корутина, по требованию которой создается объект и ждущая, пока он вернется, чтобы продолжить с ним работать

2 SpawnFromPool- корутина внутри пула, которая ищет уже созданный объект в пуле, если его нет, создает новый.

3 GetNew- корутина создает новый объект через LoadAsync и возвращает в первый метод через переменную типа GameObject, которая все это время передавалась по цепочке корутин.

Вопросы:

1- Как обойтись одной корутиной, сохранив возможности пула?

2- Чтобы получить объект в стартовую корутину, мне приходится создавать новый объект и передавать его в последующие корутины. К сожалению, нельзя передать пустую переменную GameObject go, нужно присвоить ей new GameObject и потом передавать в методы. Получается, мы только зря расходуем ресурсы на создание не нужного объекта. Как передать пустую ссылку на gameObject для заполнения?
Инженер
UNIт
 
Сообщения: 88
Зарегистрирован: 22 май 2016, 11:13

Re: Как сделать пул объектов с асинхронной загрузкой ресурсов?

Сообщение Инженер 16 июн 2020, 12:12

Я делаю это все для системы стриминга открытого мира.

На сколько я понял, ResourceReques можно кэшировать один раз и потом из него инстансить сколько угодно копий без обращения к жесткому диску. Тогда можно при старте игры загрузить все префабы всей игры в память, а во время игры уже синхронно (без всяких корутин и т.д.) инстансить нужное количество копий, не трогая ж/д. Вот только я пока не знаю, сколько всего будет ресурсов в будущих играх (расчет не на мобилки, а на пк). Если в игре есть сотня моделек с 4К текстурами альбедо, металлик и нормал, это вполне может занять 6-8 Гб оперативы, а если моделей будет больше на порядок? И это без учета звуков, которые тоже будут подтягиваться в память вместе с ResourceReques. Так что, вероятно, это не вариант.

Можно синхронно инстансить пустышки с нужными скриптами (я обращаюсь к объектам по главному компоненту, в котором хранятся все универсальные методы для работы с объектом). Делать с ним что-то в синхронном режиме, т.к. обычно требуются компоненты Transform и этот кастомный главный компонент. А уже сам объект подтягивает на себя в асинхронном режиме тяжелые ассеты: текстуры, звуки, меш и т.д. Хотя вариант тоже кривой какой-то. Если мне однажды сразу после инстанса захочется вершины в меше передвинуть, а меш еще не подгрузился? Городить всякие проверки и корутины опять...

Можно переписать пул так, что объект там не только инстансится, но и все работы с ним проводятся в пуле. То есть мне нужен объект "prefabID = 10", после чего мне нужно десериализовать в него некие данные из сэйва. Посылаю в пул этот ID + сериализованные данные объекта. В пуле в корутине грузится ресурс, инстансится объект, десериализуются данные и объект добавляется в наблюдаемый список объектов сцены (я должен иметь такой список). Вся идея пула как изолированной системы рушится, потому что теперь он крепко связан с системой стриминга.

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

Еще один крайний вариант: начать изучение ECS. По крайней мере, там не нужен пул. Хотя ресурсы все равно грузятся асинхронно с ж/д, но вся логика системы будет другой, может мне будет проще сделать стриминг открытого мира там. Хотя сейчас ECS я АБСОЛЮТНО не понимаю, там магия какая-то. И нормальных руководств для "чайников" на русском нету.

В общем, пока выхода не вижу. Застопорился на этих асинхронных штуках.
Инженер
UNIт
 
Сообщения: 88
Зарегистрирован: 22 май 2016, 11:13

Re: Как сделать пул объектов с асинхронной загрузкой ресурсов?

Сообщение Инженер 19 июн 2020, 14:32

Jarico писал(а):Кэширует если загружается из Resources, с бандлами кэширует если только бандл выгружен с помощью Unload(false)


Так я не понял, стоит кэшировать или нет? Если я уже один раз загрузил ресурс с жесткого диска и не вызывал Resources.UnloadUnusedAssets, Resources.Load будет в следующие разы загружать с жесткого диска или из памяти?
Инженер
UNIт
 
Сообщения: 88
Зарегистрирован: 22 май 2016, 11:13


Вернуться в Скрипты

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 5