0
\$\begingroup\$

I have a very large environment which I have to load/unload in unity Webgl as I can't load them at once. So, I have divide the environment into 1000 x 1000m (1km) (each chunk is consist of three layer like objects layer, tree layer,floor layer) chunks and on my player I have attached this script for chunk loading/unloading, thanks to this guide.

    public class TileLoadingManager : MonoBehaviour
    {
        //[SerializeField]
        public string tileTag;

        //[SerializeField]
        public Vector3 tileSize;

        //[SerializeField]
        public int maxDistance;

        public GameObject[] tiles;

        // Use this for initialization
        void Start()
        {
            this.tiles = GameObject.FindGameObjectsWithTag(tileTag);
            DeactivateDistantTiles();
        }

        void DeactivateDistantTiles()
        {
            Vector3 playerPosition = this.gameObject.transform.position;

            foreach (GameObject tile in tiles)
            {
                Vector3 tilePosition = tile.gameObject.transform.position + (tileSize / 2f);

                float xDistance = Mathf.Abs(tilePosition.x - playerPosition.x);
                float zDistance = Mathf.Abs(tilePosition.z - playerPosition.z);

                if (xDistance + zDistance > maxDistance)
                {
                    tile.SetActive(false);
tile.GetComponent<PerRegionABCaller>().AbLoadCall();

                }
                else
                {
                    tile.SetActive(true);
 tile.GetComponent<PerRegionABCaller>().AbUnLoadCall();
                }
            }
        }

        void Update()
        {
            DeactivateDistantTiles();

        }
    }

Its loading/unloading my chunks but a problem is that, it is a costly method for load/unload as my object chunk (tile) count is around 4 (expected to grow). Additionally I am loading asset bundle from UnityWebReqest and destroying assets on-unload, so it very heavy task. So my question is that is this the right approach or I should need to adopt anything else?

EDIT: Bundle Loading Code Snippet:

public void AbLoadCall()
    {
        if (AbLoadCallCortoutineRef == null)
        {
            AbLoadCallCortoutineRef = StartCoroutine(AbLoadCallCoroutine());
        }
    }

    IEnumerator AbLoadCallCoroutine()
    {
        for (int i = 0; i < perRegionAssetBundleList.Length; i++)
        {
            yield return StartCoroutine(perRegionAssetBundleList[i].DownloadABCall());
        }

    }

Actual Loading Code:

    public IEnumerator DownloadABCall()
    {
        if (!abObjectInstantiated)
        {
            //Debug.Log(assetName + " , downloading assetbundle. . ");
            yield return StartCoroutine(DownloadAB(assetBundleURL));
        }
        else
        {
            //Debug.Log("Reactivintg");
            if (!abObjectInstantiated.activeInHierarchy)
            {
                //Debug.Log("Reactive done"+ this.assetName);
                abObjectInstantiated.SetActive(true);
            }

            if (floorL7MeshRenderer)
            {
                floorL7MeshRenderer.enabled = false;
            }

        }
        //else
        //{
        //    Debug.Log(this.name + " , Bundle object has alread available.");
        //}
    }

 IEnumerator DownloadAB(string bundleURL, string assetName = "")
    {
        Debug.Log(". . DownloadAB. . ,"+ this.name);
        isBundleLoading = true;

        www = UnityWebRequestAssetBundle.GetAssetBundle(bundleURL);
        //Debug.Log("web req about to send : " + assetName);

        yield return www.SendWebRequest();

        if (www.error != null)
        {
            Debug.LogError("assetBundleURL : " + assetBundleURL);
            Debug.LogError("www error : " + www.error);
            www.Dispose();
            www = null;
            yield break;
        }

        //Debug.Log("bundle loading , "+ assetName);

        // get bundle from downloadhandler
        AssetBundle bundle = ((DownloadHandlerAssetBundle)www.downloadHandler).assetBundle;

        GameObject bundlePrefab = null;

        bundlePrefab = (GameObject)bundle.LoadAsset(bundle.GetAllAssetNames()[0]);


        // if we got something out
        if (bundlePrefab != null)
        {

            abObjectInstantiated = (GameObject)Instantiate(bundlePrefab);
            abObjectInstantiated.transform.parent = envParent.transform;

            if (floorL7MeshRenderer)
            {
                floorL7MeshRenderer.enabled = false;
            }  

        }

        www.Dispose();
        www = null;

        Resources.UnloadUnusedAssets();
        bundle.Unload(false);
        bundle = null;

        isBundleLoading = false;
        //Debug.Log("bundle unloading completed ," + assetName);
    }
\$\endgroup\$
  • \$\begingroup\$ Would you like to explain more about your game flow? When do you exactly want to load/unload your assets according to your game flow. What datatypes are with the bundles? \$\endgroup\$ – Saad Anees Apr 1 at 8:20
  • \$\begingroup\$ There are some objects (buildings floor etc) which i want to load and unload based on certain distance from my player(camera). \$\endgroup\$ – Muhammad Faizan Khan Apr 1 at 9:09
  • \$\begingroup\$ Can you show me what you are doing at tile.GetComponent<ABLoader>().LoadBundle();? \$\endgroup\$ – Saad Anees Apr 1 at 10:43
  • \$\begingroup\$ @SaadAnees I have updated the code. \$\endgroup\$ – Muhammad Faizan Khan Apr 1 at 11:12
2
+25
\$\begingroup\$

I am assuming you are loading AssetBundle at Start. If so then your method DeactivateDistantTiles() is too expensive to be called in Update() since its been called every frame and your Coroutines are nested and in a for loop and telling Update method to delay which is wrong until and unless it is being validated properly. So you need to change your code structure a little bit.

If you are loading multiple assets so make sure to make a list and add your items to that list. When you are done downloading assets use that list in your game. For example:

List<GameObject> listOfGameObjects = new List<GameObject>();

In your case:

if (bundlePrefab != null)
{
    listOfGameObjects.Add(bundlePrefab);
}

Then in Update you can do something like this: (You can modify according to your need)

public void Update()
{
    if(HasDownloadFinished)
    {
        foreach (var bundle in listOfGameObjects)
        {
            abObjectInstantiated = (GameObject)Instantiate(bundle);
            abObjectInstantiated.transform.parent = envParent.transform;

            if (floorL7MeshRenderer)
            {
                floorL7MeshRenderer.enabled = false;
            }
        }
    }
    else if(isDownloading)
    {
        //Restrict player to go beyond your Game View screen.
        //Start downloading chunks again here
    }

}

Also try to use CachedAssetBundle. Its useful when you download assets frequently. According to Unity's docs

The caching system keeps track of the last version number passed to UnityWebRequest. When this API is called with a version number, the caching system checks to see if there is a cached AssetBundle by comparing version numbers. If these numbers match, the system will load the cached AssetBundle. If the numbers do not match, or there is no cached AssetBundle, then Unity will download a new copy. This new copy will be associated with the new version number.

For more information about built-in caching AssetBundle, click here.

\$\endgroup\$
  • \$\begingroup\$ Sorry but your solution is not feasilbe for the problem. I can't load all assts at start i have load the assets dynamically based on camera position. additionally, Unity Built-in cache system is a crap that why i am not using it, i am using unity alternate plugin(XMLHttpRequest for it, it provide caching as well. \$\endgroup\$ – Muhammad Faizan Khan Apr 1 at 13:41
1
\$\begingroup\$

I would suggest that you sort these chunks into a lookup table, either a 2D array GameObject[,] or a Dictionary<int2, GameObject>. For each of your chunks you would then store it in your lookup on the index int2(floor(x / tileSize), floor(z / tileSize)).

When iterating your chunks you only have to go over a certain number of indices, rather than going over the whole list of chunks. In pseudo code this could look like:

int maxDist = ceil(maxDistance / tileSize);
int2 myPos = new int2(floor(me.x / tileSize), floor(me.z / tileSize));
for (int y = myPos.y - maxDist; y < myPos.y + maxDist; y++)
  for (int x = myPos.z - maxDist; z < myPos.z + maxDist; z++)
    if (chunkLookup.TryGetValue(new int2(x, y), out var chunk)
      if (!chunk.active)
        activate(chunk);
        activeList.Add(chunk);

By storing all active chunks in a list, you can easily iterate over the active chunks, to see which ones to unload, and remove from the active list.

Edit: You might need to enable Unity.Mathematics in the Package Manager, in order to use int2.

\$\endgroup\$
  • \$\begingroup\$ Thanks for your answer, can you define your code a bit more. I tried to understand and implement it but unable to grip the concept completely. \$\endgroup\$ – Muhammad Faizan Khan Mar 30 at 10:01
  • \$\begingroup\$ What Jannek is describing is nearly identical to a spatial hash map. - zufallsgenerator.github.io/2014/01/26/… - zufallsgenerator.github.io/2014/01/26/… \$\endgroup\$ – Blue_Pyro Mar 31 at 0:44
  • \$\begingroup\$ What you basically do is split your world into chunks then only use the chunks touching and/or near your target object. Or you can get an area and get all chunks in that area for things like rendering. It's hella handy. \$\endgroup\$ – Blue_Pyro Mar 31 at 0:48
0
\$\begingroup\$

The simplest thing I can think of right now would be to change it into much smaller chunks (something like Minecraft's 16x16 chunks) but maybe more like 64x64. If any chunks are within, say, 256 units of the player, then they will be loaded. Once they go outside of that, they would be unloaded, and any new chunks would be then loaded.

I am sorry, but I cannot think of any code for it as my computer with unity on it is currently being repaired.

But that should not be too hard to implement.

\$\endgroup\$
  • \$\begingroup\$ but the question is how do i know that which chunk i load? \$\endgroup\$ – Muhammad Faizan Khan Apr 4 at 14:35
  • \$\begingroup\$ with that, you could have a function that generates a numerical value of the chunk to be accessed (if the chunkmap would be 100 x 100, then the chunk id for the chunk at x, y on the chunk map could be 100*x+y) then you access that specific chunk through, say, a post http request to the server holding the chunk data. \$\endgroup\$ – Caleb Evans Apr 19 at 22:05
  • \$\begingroup\$ But how do i will find that my camera is within range of a specific chunk? \$\endgroup\$ – Muhammad Faizan Khan Apr 22 at 4:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.