Unity WebGL doesn't properly unload and load Addressable scenes - unity3d

I am trying to unload a previously added scene and then additively load another scene, in that sequence. I use the following code below to do this. This code works correctly in the editor and on all platforms except WebGL. I know that WebGL won't work with asynchronous code, but this uses a coroutine, which should be executed on the main thread. So, why would this not work in WebGL?
private IEnumerator UnloadAndLoadNextScene(IslandKey island)
{
// Avoid loading the same island twice.
if (island.ScenePath == previousIslandScenePath)
yield break;
string islandScenePath = island.ScenePath;
previousIslandScenePath = islandScenePath;
// Don't unload and load until previous unload and load is complete.
while (processingUnloadAndLoad)
yield return null;
// Immediately set processing unload and load to true to
// lock this method to only one instance at a time.
processingUnloadAndLoad = true;
// Unload previous scene.
if (unloadSceneHandle.IsValid())
{
AsyncOperationHandle unloadingScene = Addressables.UnloadSceneAsync(unloadSceneHandle);
yield return unloadingScene;
}
// Load scene.
AsyncOperationHandle<SceneInstance> loadingScene = Addressables.LoadSceneAsync(islandScenePath, LoadSceneMode.Additive);
yield return loadingScene;
if (loadingScene.Status == AsyncOperationStatus.Succeeded)
unloadSceneHandle = loadingScene;
...
// Unload and load is now complete, so unlock method.
processingUnloadAndLoad = false;
}
}

Related

Optimal way for managing async loading of assets on game load

I'm developing a mobile idle game for iOS and Android and I'm now at the stage where I'm building the game's save/load infrastructure.
When opening the game app for loading a previously saved session, after the app has been closed, and before the loading the previously saved session, I'm loading mandatory game assets, mostly ScriptableObjects, with Addressables asynchronously like this:
StartCoroutine(LoadInventoryAssets());
StartCoroutine(LoadCharacterAssets());
StartCoroutine(LoadSoundAssets());
Each LoadAssets function looks roughly like this:
_assetsLoadingOp = Addressables.LoadAssetsAsync<AssetType>("AssetLabel", null);
yield return _assetsLoadingOp;
if (_assetsLoadingOp.IsDone)
{
_results = _assetsLoadingOp.Result
}
Finally, when all of these are loaded I run the function that loads the relevant scene, game objects, and injects the saved data. This function must only run after all the previous Addressable assets were loaded.
yield return StartCoroutine(LoadSavedGame());
The problem with the above is that the LoadSavedGame function might run before all of the previous asset loading functions were completed.
I could solve that by adding yield return to all Coroutines, but wouldn't that make each of them load sequentially instead of in parallel?
Any thoughts on what's the best approach for this would be greatly appreciated.
You can just use an integer to count how many functions have been complete. In the end of each LoadAssets function:
if(++task == 3)
StartCoroutine(LoadSavedGame());
OK so eventually I went with this approach:
bool? inventoryAssetsLoadedResult = null, characterAssetsLoadedResult = null, soundAssetsLoadedResult = null;
StartCoroutine(LoadInventoryAssets((result)=> inventoryAssetsLoadedResult = result));
StartCoroutine(LoadCharacterAssets((result)=> characterAssetsLoadedResult = result));
StartCoroutine(LoadSoundAssets((result)=> soundAssetsLoadedResult = result));
yield return new WaitUntil(() => inventoryAssetsLoadedResult.HasValue & characterAssetsLoadedResult.HasValue & soundAssetsLoadedResult.HasValue);
Each coroutine looks similar to this:
public IEnumerator LoadInventoryAssets(Action<bool> callback)
{
yield return new WaitUntil(() => _inventoryLoadingOp.IsDone);
callback(true);
}
Any suggestions for improvements are welcome.

How is Scene.isLoaded determined?

I cannot find anything in docs that breaks down the scene loading process and how the Scene.isLoaded property is determined.
I need to mock large scene loading and cannot find a way to delay a scene's loading. It would be useful to understand the flow especially when needing to load external assets in the scene and only mark the scene as loaded via code.
You should have a look at SceneManager.LoadSceneAsync and especially allowSceneActivation which allows you to do exactly that: Delay a scenes loading and e.g. display a loading screen meanwhile.
Example from the docs:
// This script lets you load a Scene asynchronously.
// It uses an asyncOperation to calculate the progress and outputs
// the current progress to Text (could also be used to make progress bars).
// Attach this script to a GameObject
// Create a Button (Create>UI>Button) and a Text GameObject (Create>UI>Text)
// and attach them both to the Inspector of your GameObject
//In Play Mode, press your Button to load the Scene, and the Text
// changes depending on progress. Press the space key to activate the Scene.
//Note: The progress may look like it goes straight to 100% if your Scene doesn’t have a lot to load.
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class AsyncOperationProgressExample : MonoBehaviour
{
public Text m_Text;
public Button m_Button;
void Start()
{
//Call the LoadButton() function when the user clicks this Button
m_Button.onClick.AddListener(LoadButton);
}
void LoadButton()
{
//Start loading the Scene asynchronously and output the progress bar
StartCoroutine(LoadScene());
}
IEnumerator LoadScene()
{
yield return null;
//Begin to load the Scene you specify
AsyncOperation asyncOperation = SceneManager.LoadSceneAsync("Scene3");
//Don't let the Scene activate until you allow it to
asyncOperation.allowSceneActivation = false;
Debug.Log("Pro :" + asyncOperation.progress);
//When the load is still in progress, output the Text and progress bar
while (!asyncOperation.isDone)
{
//Output the current progress
m_Text.text = "Loading progress: " + (asyncOperation.progress * 100) + "%";
// Check if the load has finished
if (asyncOperation.progress >= 0.9f)
{
//Change the Text to show the Scene is ready
m_Text.text = "Press the space bar to continue";
//Wait to you press the space key to activate the Scene
if (Input.GetKeyDown(KeyCode.Space))
//Activate the Scene
asyncOperation.allowSceneActivation = true;
}
yield return null;
}
}
}
Also checkout sceneLoaded: as stated in the docs
Add a delegate to this to get notifications when a Scene has loaded.
This happens after the scene has been loaded and afaik after the Awake calls finished. You can easily test this by using a script like
public class Test : MonoBehaviour
{
private void Awake()
{
Debug.Log("Awake");
SceneManager.sceneLoaded += OnSceneLoaded;
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
Debug.Log("OnSceneLoaded");
}
}
You will see both the
Awake
OnSceneLoaded
debugs in the console meaning that Awake was called before OnSceneLoaded.
&rightarrow; At this moment I would also expect the Scene.isLoaded to be set to true.

Keeping variable from changing on another scene load

I'm new to unity and i'm trying to load scenes based on items collected.
Problem is that the counter is not counting my acquired items.
I'm using OnTriggerEnter2D() to trigger the event; Below is the snippet:
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.gameObject.CompareTag("Player"))
{
collectionNumber += 1;
Destroy(gameObject);
if (collectionNumber == 1)
{
collision.gameObject.transform.Find("Acquired_Items").GetChild(0).gameObject.SetActive(true);
qiCollector.gameObject.transform.GetChild(0).gameObject.SetActive(true);
}
else if (collectionNumber == 2)
{
collision.gameObject.transform.Find("Acquired_Items").GetChild(1).gameObject.SetActive(true);
qiCollector.gameObject.transform.GetChild(1).gameObject.SetActive(true);
}
else if (collectionNumber == 3)
{
collision.gameObject.transform.Find("Acquired_Items").GetChild(2).gameObject.SetActive(true);
qiCollector.gameObject.transform.GetChild(2).gameObject.SetActive(true);
}
else
{
Debug.LogWarning("All Items Collected !!");
}
cN.text = "Collection Number " + collectionNumber.ToString();
}
}
Whenever a new scene is loaded this script is loaded because it is on my quest item. And for every scene there is a quest item. So what I want to do is basically keep track of my collectionNumber, but it resets to 0.
Any help is much appreciated :)
First method:
Don't allow your object to be destroyed on scene load
https://docs.unity3d.com/ScriptReference/Object.DontDestroyOnLoad.html
public static void DontDestroyOnLoad(Object target);
Above code will prevent destroying your GameObject and its components from getting destroyed when loading a new scene, thus your script values
Second Method:
Write out your only value into a player pref
https://docs.unity3d.com/ScriptReference/PlayerPrefs.html
// How to save the value
PlayerPrefs.SetInt("CollectionNumber", collectionNumber);
// How to get that value
collectionNumber = PlayerPrefs.GetInt("CollectionNumber", 0);
Third method:
Implement a saving mechanism:
In your case i would not suggest this

How do I change GameObject properties in scripts Unity?

I'm trying to save/load my game. In the load method, everytime I change the properties of a GameObject, those changes are applied and then get reverted shortly after. Here is my code:
public void Load()
{
SceneManager.LoadScene(sceneID);
List<GameObject> rootObjects = new List<GameObject>();
Scene scene = SceneManager.GetActiveScene();
scene.GetRootGameObjects(rootObjects);
int ncube = 0, npick = 0;
for (int i = 0; i < rootObjects.Count; ++i)
{
GameObject obj = rootObjects[i];
if (obj.CompareTag("Player"))
{
obj.transform.position = player.position;
obj.transform.rotation = player.rotation;
obj.transform.localScale = player.localScale;
}
else if (obj.CompareTag("Cube"))
{
obj.transform.position = cube[ncube].position;
obj.transform.rotation = cube[ncube].rotation;
obj.transform.localScale = cube[ncube].localScale;
++ncube;
}
else if (obj.CompareTag("Pickup"))
obj.SetActive(pickup[npick++]);
else if (obj.CompareTag("Door"))
obj.SetActive(door);
else if (obj.CompareTag("GreenWall"))
obj.SetActive(greenWall);
}
}
Those changes are applied to the GameObject, however they get aborted right away. How can I resolve this?
The script contains these lines of code is not a component of the GameObject.
Edit 1: Complete code updated.
Problem is I think that
Scene scene = SceneManager.GetActiveScene();
scene.GetRootGameObjects(rootObjects);
gets the objects from the scene before the Scene is fully loaded so they are reset.
From the Docu
When using SceneManager.LoadScene, the loading does not happen immediately, it completes in the next frame. This semi-asynchronous behavior can cause frame stuttering and can be confusing because load does not complete immediately.
I guess you rather should use SceneManager.sceneLoaded and do your stuff there like
public void Load()
{
SceneManager.LoadScene(sceneID);
}
And maybe in an extra component within the scene:
void OnEnable()
{
SceneManager.sceneLoaded += OnSceneLoaded;
}
void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
// your stuff here
}
void OnDisable()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
though we don't know/see where player, cube[ncube] etc come from ...
for transparting values between Scenes you should get into using ScriptableObjects
The problem might be that SceneManager.LoadScene completes in the next frame. See the documentation: https://docs.unity3d.com/ScriptReference/SceneManagement.SceneManager.LoadScene.html
It says:
When using SceneManager.LoadScene, the loading does not happen
immediately, it completes in the next frame. This semi-asynchronous
behavior can cause frame stuttering and can be confusing because load
does not complete immediately.
You change the values within the same frame, thus they are overwritten when loading the scene finishes. Cou could use an event to prevent that behaviour: https://docs.unity3d.com/ScriptReference/SceneManagement.SceneManager-sceneLoaded.html

Add remote image as texture during loop

I am very new to Unity3d.
I have a JSON array that contains the parameters of the prefabs I want to create at runtime.
I want to display images that are stored on my server in the scene.
I have a prefab "iAsset" that has a plane (mesh filter) and I want to load the image files as the texture of the plane.
I am able to Instatiate the prefabs however the prefab is showing up as a white square. This is my code:
for(var i = 0; i < bookData.Assets.Count; i++){
GameObject newAsset = null;
newAsset = (GameObject)Instantiate(iasset, new Vector3(2*i, 0, 0), Quaternion.identity);
if(!imageAssetRequested )
{
remoteImageAsset = new WWW(((BookAssets)bookData.Assets[i]).AssetContent);
imageAssetRequested = true;
}
if(remoteImageAsset.progress >=1)
{
if(remoteImageAsset.error == null)
{
loadingRemoteAsset = false;
newAsset.renderer.material.mainTexture = remoteImageAsset.texture;
}
}
}
the urls to the images on my server is retrieved from the JSON array:
((BookAssets)bookData.Assets[i]).AssetContent);
The code builds without any errors, I would very much appreciate any help to display the remote images.
You are not waiting for your downloads to complete.
The WWW class is asynchronous, and will commence the download. However, you need to either poll it (using the code you do have above) at a later time, or use a yield WWW in a CoRoutine that will block your execution (within that CoRoutine) until the download finishes (either successfully or due to failure).
Refer to Unity Documentation for WWW
Note however, that page sample code is wrong, and Start is not a CoRoutine / IEnumarator. Your code would look something like :
void Start()
{
... your code above ...
StartCoroutine(DownloadImage(bookData.Assets[i]).AssetContent, newAsset.renderer.material.mainTexture));
}
IEnumerator DownloadImage(string url, Texture tex)
{
WWW www = new WWW(url);
yield return www;
tex.LoadImage(www.bytes)
}