I need to wait until a scene is fully loaded in order to move a gameObject to it from DontDestroyOnLoad scene. If I do it too soon (just after calling SceneManager.LoadScene()) then the gameObject disappears. Based on this post I implemented a scene loading class to solve this issue:
public static class CustomSceneManager
{
public delegate void SceneChange(string sceneName);
public static event SceneChange LoadScene;
public static event SceneChange UnloadScene;
private static IEnumerator LoadLevel (string sceneName){
var asyncLoadLevel = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Single);
while (!asyncLoadLevel.isDone){
Debug.Log("Loading the Scene");
yield return null;
}
}
public static void OnLoadScene(string newSceneName)
{
OnUnloadScene(newSceneName);
LoadLevel(newSceneName);
LoadScene?.Invoke(newSceneName);
}
private static void OnUnloadScene(string newSceneName)
{
UnloadScene?.Invoke(newSceneName);
}
}
I'm calling two events from it (LoadScene and UnloadScene). However the LoadLevel(newSceneName) doesn't work - it simply doesn't load a scene. What am I doing wrong here?
EDIT:
Now I'm passing the MonoBehavior reference of the script calling OnLoadScene methid like this:
public static void OnLoadScene(MonoBehaviour loader, string newSceneName)
{
UnloadScene?.Invoke(newSceneName);
loader.StartCoroutine(LoadLevel(newSceneName));
Debug.Log(SceneManager.GetActiveScene().name); // this line returns previous scene
LoadScene?.Invoke(newSceneName);
}
Now the scene loads, but when I check what scene is currently loaded, it returns the previous scene name.
EDIT 2:
To be more precise I replaced Debug.Log(SceneManager.GetActiveScene().name); with Debug.Log(SceneManager.GetSceneByName(newSceneName).isLoaded); and it returns False.
You have to run coroutienes using StartCoroutine.
You either would need to pass in a reference of a MonoBehaviour that will execute the coroutine or simply make your class a Singleton that is never destroyed
Than actually you will invoke your event too early when it is not yet loaded but you just started to load it so rather do e.g.
public class CustomSceneManager : MonoBehaviour
{
public delegate void SceneChange(string sceneName);
public static event SceneChange LoadScene;
public static event SceneChange UnloadScene;
private CustomNetworkManager singleton;
private void Awake ()
{
if(singleton && singleton != this)
{
Destroy(gameObject);
}
singleton = this;
DontDestroyOnLoad (gameObject);
}
private static IEnumerator LoadLevel (string sceneName){
var asyncLoadLevel = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Single);
while (!asyncLoadLevel.isDone){
Debug.Log("Loading the Scene");
yield return null;
}
LoadScene?.Invoke(newSceneName);
}
public static void OnLoadScene(string newSceneName)
{
if(! singleton)
{
singleton = new GameObject("CustomNetworkManager").AddComponent<CustomNetworkManager>();
}
OnUnloadScene(newSceneName);
singleton.StartCoroutine(LoadLevel(newSceneName));
}
Another way you can do this is with an async method.
The catch is that in order to await an AsyncOperation you need a custom awaiter class. There are a few libraries that make it possible, and my favorite one is UniTask.
using Cysharp.Threading.Tasks;
using UnityEngine.SceneManagement;
public static class CustomSceneManager
{
public delegate void SceneChange(string sceneName);
public static event SceneChange LoadScene;
public static event SceneChange UnloadScene;
private static async UniTask LoadLevelAsync(string sceneName)
{
await SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Single);
}
public static async UniTask OnLoadSceneAsync(string newSceneName)
{
OnUnloadScene(newSceneName);
await LoadLevelAsync(newSceneName);
LoadScene?.Invoke(newSceneName);
}
private static void OnUnloadScene(string newSceneName)
{
UnloadScene?.Invoke(newSceneName);
}
}
Related
For example, there's two classes here.
Class One:
using UnityEngine;
using System.Collections.Generic;
public class One:MonoBehavior
{
private List<string> _assetBundleList;
private void Start()
{
InitializeList();
}
private void InitializeList()
{
//Add assetBundleList field some value.
}
public IEnumerator<string> GetEnumerator()
{
return _assetBundleList.GetEnumerator();
}
}
Class two:
public class Two:MonoBehavior
{
public GameObject gameObjectWithScriptOne;
private One _scriptOne;
private void Start()
{
scriptOne = gameObjectWithScriptOne.GetComponent<One>();
DoSomething();
}
private void DoSomething()
{
foreach(var assetBundle in scriptOne)
{
//Load asset
}
}
}
Script one is just like a manager things, I use this for storing asset bundle data, perhaps the list value will change. Script two must wait for initializing done. Is there any way to wait for it except adjusting script order?
It can be handled with easily creating a context that initialize things sequentially. In short you need to block the main thread.
Let's say you have a root or persistent scene that do must things like loading assets etc.
//The only object in the Root Scene.
public class RootContext : MonoBehaviour
{
private const string _playerAddress = "Player";
public void Awake()
{
//Load assets
//wait for assets loading
//instantiate things.
//new instantiated things can also initalize with this way.
var handle = Addressables.LoadAssetAsync<GameObject>(_playerAddress);
var asset = handle.WaitForCompletion();
var go = Instantiate(bla bla bla);
}
}
I think the what you want is a framework that organize things. You can look at Strange. It is MVCS + IoC framework. Addionality what you said about "when field or property initialization is done" with [PostConstruct] attribute with Strange.
[Inject(ContextKeys.CONTEXT)]
public IContext Context { get; set; }
/*Initialization of injections are guaranteed at here. Their initialization
is also quaranteed like this class.
*/
[PostConstruct]
public void Initalize()
{
}
I'd go with refactoring, but if this is not the case C# events might be the solution.
Class One
public class One:MonoBehavior
{
private List<string> _assetBundleList;
public event Func Initialized;
private void Start()
{
InitializeList();
}
private void InitializeList()
{
//Add assetBundleList field some value.
Initialized?.Invoke();
}
public IEnumerator<string> GetEnumerator()
{
return _assetBundleList.GetEnumerator();
}
}
Class Two:
public class Two:MonoBehavior
{
public GameObject gameObjectWithScriptOne;
private One _scriptOne;
private void Start()
{
scriptOne = gameObjectWithScriptOne.GetComponent<One>();
scriptOne.Initialized += DoSomething;
}
private void DoSomething()
{
foreach(var assetBundle in scriptOne)
{
//Load asset
}
}
}
Also, you should unsubscribe from events when disabling an object, but you'll figure it out by yourself.
The Idea is, that my Data get loaded, asynchron, when my app startet. Because the user normaly need some seconds to interact and in this time, i should get my data. (For understanding reasons, it will be a book app, with some books)
To do this, in loadingprofile i open a method on a static class to start loading.
In the Screen where i want to show my data, i check with a method on the static class, if the data are there ( and wait if theire not).
So in the Profil loading screen i did this:
public class LoadProfiles : MonoBehaviour
{
private void Awake()
{
if (!LoadingData.dataLoaded)
{
LoadingData.LoadData();
}
}
void Start()
{
//Loading Profile Dataa
}
}
Then when the User click on a profile, i open the next scene "Bookshelf" and a game object in it has the following script attachted to read the data (the book categories). so i open the wait on data method, that shoud return, when the data are arrived.
public class PopulateShelf : MonoBehaviour
{
private void Start()
{
LoadingData.WaitOnData();
LoadingData.Categories.ForEach(c => {
//create categories
});
}
}
The static loading class looks like the following
public class LoadingData : MonoBehaviour
{
public static List<CategoryDTO> Categories;
public static IEnumerator CategoriesRequest;
public static bool dataLoaded = false;
public static void LoadData()
{
CategoriesRequest = LoadCategories();
dataLoaded = true;
}
public static void WaitOnData()
{
if (!dataLoaded) LoadData();
while (CategoriesRequest.MoveNext())
{
//Log wait on categories
}
}
public static IEnumerator LoadCategories()
{
UnityWebRequest request = UnityWebRequest.Get(Constants.apiURL + Constants.apiURLCategories);
yield return request.SendWebRequest();
while (!request.isDone)
{
yield return true;
}
if (request.result == UnityWebRequest.Result.ConnectionError)
{
//Log error
}
else
{
Categories = JsonConvert.DeserializeObject<List<CategoryDTO>>(request.downloadHandler.text);
Categories.Sort((x, y) => x.Order.CompareTo(y.Order));
}
}
}
So, the problem is that the on the loging screen, it opens the LoadingData.LoadData Method (but it do not start loading).
When im in the Bookshelf Scene, and open LoadingData.WaitOnData it starts loading the data and the User have to wait.
I also tried it with StartCoroutines. I refactored the LoadingData class, to a non static class. Startet the Coroutings in the LoadData Method.
First it looked like it worked, the data was loaded in the profileScene
Because of it was not static, i had to pass somehow the Object. To do this i created in a static script a Constants for a static LoadingData script variable.
In the Profile Scene i attached it and loaded the data. When i wanted to read it in the BookShefScene, i got a null refrence in my static class...
Here would be the class with coroutines
public class LoadingData : MonoBehaviour
{
public Dictionary<int, BookDTO> Books = new Dictionary<int, BookDTO>();
public List<CategoryDTO> Categories;
public bool dataLoaded = false;
public void LoadData()
{
StartCoroutine(LoadCategories());
StartCoroutine(LoadBooks());
dataLoaded = true;
}
public void WaitOnData()
{
if (!dataLoaded) LoadData();
while (categorieLoaded)
{
//log waiting
}
}
public IEnumerator LoadCategories()
{
UnityWebRequest request = UnityWebRequest.Get(Constants.apiURL + Constants.apiURLCategories);
yield return request.SendWebRequest();
while (!request.isDone)
{
yield return true;
}
if (request.result == UnityWebRequest.Result.ConnectionError)
{
//Log Error
}
else
{
Categories = JsonConvert.DeserializeObject<List<CategoryDTO>>(request.downloadHandler.text);
Categories.Sort((x, y) => x.Order.CompareTo(y.Order));
}
}
}
in profile loading screen, to attache my script to the static variable, i said something like this:
Constants.loadingData = this.GetComponent<LoadingData>();
Constants.loadingData.LoadData();
and in the bookshelf i said
Constants.loadingData.WaitOnData();
Constants.loadingData.Categories();
I checked with the Debuger, in the Profile Scene the Constants.loadingData was created and referenced.
In the Bookshelf Scene, i dont saw the referenced object (it was null)...
can anyoune help me with one of this solution? or have a third solution?
I think your condition should be
if(!dataLoaded) LoadData();
Same for the categorieLoaded, maybe the name is misleading and you are using them correctly (?)
Is the GameObject stored in Constants.loadedData set as dontDestroyOnLoad? Maybe when you change scene it gets destroied and set to null.
Also I would suggest to have a better way of waiting for the data to load, if it takes seconds the user will not be able to interract with the game since the while loop will freeze the entire window, you can make your own method that checks every frame if the data got loaded.
private IEnumerable WaitForData()
{
while(!Constants.loadedData.Loaded) return null;
// populate the bookshelf
// foreach(Book book in Constants.Books)
}
You need to call LoadData() somewhere before calling this, you could setup a loading screen while waiting aswell to make players understand.
I have a script that consist of an event and a function that is triggered when this is called. Now I have another script. How do I check if the event from first script is triggered?
First script
public class MyEventManager: MonoBehaviour
{
private static MyEventManager _instance;
public static MyEventManager Instance
{
get
{
if (_instance == null)
_instance = FindObjectOfType<MyEventManager>();
return _instance;
}
}
public delegate void OpenEventHandler(bool wasOpened);
public event OpenEventHandler Opening;
public void OpeningObj()
{
OpeningObj(true);
}
public void OpeningObj(bool wasOpened)
{
Opening?.Invoke(wasOpened);
}
Second script
private MyEventManager eventMan;
private void Start () {
eventMan= (MyEventManager ) GameObject.FindObjectOfType (typeof (MyEventManager ));
}
private void CalledFromFunction()
{
Debug.Log("Is Called");
}
I get the error - No overload for 'CalledFromFunction' matches delegate 'MyEventManager.OpenEventHandler'
Since you use a singleton approach, you don't actually need the private MyEventManager eventMan; in the other class. You can get access to the manager from anywhere with the public static property you have declared ( public static Instance MyEventManager), so to get acces to the manager just use MyEventManager.Instance, since it's static it will be accessible from anywhere.
To react to some event the listener need to subscribe to the event. It's done like this:
public class ListenerClass : MonoBehaviour
{
void Start()
{
MyEventManager.Instance.Opening += OnOpening;
}
It means whenever the Opening event is triggered in the manager, the OnOpening method of the listener class should be called:
void OnOpening(bool value)
{
// do stuff when the Opening event is triggered
}
}
EDIT: my browser failed to load the second part where you have posted the second script and the error message.
You get the error because you have declared the event delegate as a function that takes 1 boolean argument and has no return value ( void OpenEventHandler(bool wasOpened) but in the second script you set the event handler to the method that doesn't have arguments, therefore it doesn't match the delegate signature.
You need to change
private void CalledFromFunction()
to
private void CalledFromFunction(bool value)
to get the method match the delegate.
You got error No overload for 'CalledFromFunction' matches delegate 'MyEventManager.OpenEventHandler' because OpenEventHandler is a delegate with bool type parameter. So method CalledFromFunction requires a bool type parameter.
Second script:
private MyEventManager eventMan;
private void Start () {
eventMan= (MyEventManager ) GameObject.FindObjectOfType (typeof (MyEventManager ));
browserOverlay.Opening+= this.CalledFromFunction;
}
private void CalledFromFunction(bool value)
{
Debug.Log("Is Called");
}
I have a function that asynchronously loads a xml file, parses it, and adds certain values to a list. I'm using async and await for this. The issue I've run into is that after calling await the program moves on to executing code that accesses that list before the async function has finished adding all items.
My static class with async function:
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Xml.Linq;
using UnityEngine;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.AddressableAssets;
namespace Drok.Localization
{
public static class Localization
{
/// <summary>
/// The currently available languages.
/// </summary>
public static List<string> Available { get; private set; } = new List<string>();
/// <summary>
/// The currently selected language.
/// </summary>
public static string Current { get; private set; } = null;
public static async Task Initialize()
{
await LoadMetaData();
}
private static async Task LoadMetaData()
{
AsyncOperationHandle<TextAsset> handle = Addressables.LoadAssetAsync<TextAsset>("Localization/meta.xml");
TextAsset metaDataFile = await handle.Task;
XDocument metaXMLData = XDocument.Parse(metaDataFile.text);
IEnumerable<XElement> elements = metaXMLData.Element("LangMeta").Elements();
foreach (XElement e in elements)
{
string lang = e.Attribute("lang").Value;
int id = Int32.Parse(e.Attribute("id").Value);
Debug.LogFormat("Language {0} is availible with id {1}.", lang, id);
Available.Add(lang);
}
}
public static void LoadLanguage(string lang)
{
Current = lang;
throw new NotImplementedException();
}
public static string GetString(string key)
{
return key;
}
}
}
The class that initializes it and accesses the list:
using Drok.Localization;
using UnityEngine;
namespace Spellbound.Menu
{
public class LanguageMenu : MonoBehaviour
{
private async void Awake()
{
await Localization.Initialize();
}
private void Start()
{
Debug.Log(Localization.Available.Count);
}
private void Update()
{
}
}
}
I have no idea how to prevent access to that list until after all items have been added. The code I posted just collects info on what languages are available so that only the one language being used can be loaded later.
A Task<T> represents some value (of type T) that will be determined in the future. If you make your property this type, then it will force all callers to await for it to be loaded:
public static class Localization
{
public static Task<List<string>> Available { get; private set; }
static Localization() => Available = LoadMetaDataAsync();
private static async Task<List<string>> LoadMetaDataAsync()
{
var results = new List<string>();
...
results.Add(lang);
return results;
}
}
Usage:
private async Task StartAsync()
{
var languages = await Localization.Available;
Debug.Log(languages.Available.Count);
}
One possibility might be to add some logic to wait for the metadata to be loaded when returning the list from the get accessor.
One way to do this is to have a bool field that is set to true when the list is ready, and then we either return a private backing List<string> or null, depending on the value of our bool field:
public static class Localization
{
private static bool metadataLoaded = false;
private static List<string> available = new List<string>();
// The 'Available' property returns null until the private list is ready
public static List<string> Available => metadataLoaded ? available : null;
private static async Task LoadMetaData()
{
// Add items to private 'available' list here
// When the list is ready, set our field to 'true'
metadataLoaded = true;
}
}
The Awake method is async void, so there is no way for the caller to guarantee that it finishes before moving on to something else.
However, you could preserve the task and await it in the Start method to ensure that it is completed. Awaiting it twice does not harm anything.
public class LanguageMenu : MonoBehaviour
{
private Task _task;
private async void Awake()
{
_task = Localization.Initialize();
await _task;
}
private async void Start()
{
await _task;
Debug.Log(Localization.Available.Count);
}
private void Update()
{
}
}
Expanding on Rufus' comment:
Declare a bool property that's initialized to false. And in your list's getter, return the list only if the said bool property is true, and return maybe null (this depends on your requirements) if false.
public static bool IsAvailable { get; set; } = false;
private static List<string> _available;
public static List<string> Available
{
get
{
if (IsAvailable)
return _available;
else
return null;
}
set { _available = value; }
}
Finally, in your async function, when the work is done set the above property to true.
Latest when there is an Update method involved that should also wait with its execution using async and await might not be enough anyway.
Usually there is always one big alternative to using async for the Unity messages: an event system like e.g.
public static class Localization
{
public static event Action OnLocalizationReady;
public static async void Initialize()
{
await LoadMetaData();
OnLocalizationReady?.Invoke();
}
...
}
And wait for that event in any class using it like e.g.
public class LanguageMenu : MonoBehaviour
{
private bool locaIsReady;
private void Awake()
{
Localization.OnLocalizationReady -= OnLocalizationReady;
Localization.OnLocalizationReady += OnLocalizationReady;
Localization.Initialize();
}
private void OnDestroy ()
{
Localization.OnLocalizationReady -= OnLocalizationReady;
}
// This now replaces whatever you wanted to do in Start originally
private void OnLocalizationReady ()
{
locaIsReady = true;
Debug.Log(Localization.Available.Count);
}
private void Update()
{
// Block execution until locaIsReady
if(!locaIsReady) return;
...
}
}
Or for minimal better performance you could also set enabled = false in Awake and set it to true in OnLocalizationReady then you could get rid of the locaIsReady flag.
No async and await needed.
If you would move the Localization.Initialize(); instead to Start you would give other classes the chance to also add some callbacks before to Localization.OnLocalizationReady in Awake ;)
And you can extend this in multiple ways! You could e.g. together with firering the event directly also pass in the reference to Availables so listeners can directly use it like e.g.
public static class Localization
{
public static event Action<List<string>> OnLocalizationReady;
...
}
and then in LanguageMenu change the signiture of OnLocalizationReady to
public class LanguageMenu : MonoBehaviour
{
...
// This now replaces whatever you wanted to do in Start originally
private void OnLocalizationReady(List<string> available)
{
locaIsReady = true;
Debug.Log(available.Count);
}
}
If anyway LanguageMenu will be the only listener then you could even pass the callback directly as parameter to Initialize like
public static class Localization
{
public static async void Initialize(Action<List<string>> onSuccess)
{
await LoadMetaData();
onSuccess?.Invoke();
}
...
}
and then use it like
private void Awake()
{
Localization.Initialize(OnLocalizationReady);
}
private void OnLocalizationReady(List<string>> available)
{
locaIsReady = true;
Debug.Log(available.Count);
}
or as lambda expression
private void Awake()
{
Localization.Initialize(available =>
{
locaIsReady = true;
Debug.Log(available .Count);
}
}
Update
As to your question about later Initialization: Yes there is a simple fix as well
public static class Localization
{
public static event Action OnLocalizationReady;
public static bool isInitialized;
public static async void Initialize()
{
await LoadMetaData();
isInitialized = true;
OnLocalizationReady?.Invoke();
}
...
}
Then in other classes you can do it conditional either use callbacks or Initialize right away:
private void Awake()
{
if(Localization.isInitialized)
{
OnLocaInitialized();
}
else
{
Localization.OnInitialized -= OnLocaInitialized;
Localization.OnInitialized += OnLocaInitialized;
}
}
private void OnDestroy ()
{
Localization.OnInitialized -= OnLocaInitialized;
}
private void OnLocaInitialized()
{
var available = Localization.Available;
...
}
private void Update()
{
if(!Localization.isInitialized) return;
...
}
I have class "A", which will send event "a". Classes that are subscribing to event "a" will react to this event. Other classes can subscribe to event "a" without changing anything in class "A";
Now, what is the most reasonable way to do this in unity? Is there some messaging system that can already do that?
If not, should I make something like EventManager class that will store subscriber classes in array and call their methods?
There are probably many ways to do this.
Public static List
public class A : MonoBehaviour
{
public static List<A> AvailableAs = new List<A>();
private void Awake()
{
if(!AvailableAs.Contains(this)) AvailableAs.Add(this);
}
private void OnDestroy()
{
if(AvailableAs.Contains(this)) AvailableAs.Remove(this);
}
public void SomePublicMethod()
{
// magic
}
}
and use it e.g. like
public class B : MonoBehaviour
{
// run the method on all currently registered A instances
public void DoIt()
{
foreach(var a in A.AvailableAs)
{
a.SomePublicMethod();
}
}
}
Global EventHandler
Or if you rather want to go for encapsulation have as you mentioned a global event handler for all A's like
namespace ANamespace
{
public static class AEventHandler
{
internal static event Action OnInvokeA;
public static void InvokeAEvent()
{
if(OnInvokeA != null) OnInvokeA.Invoke();
}
}
}
and in A have
namespace ANamespace
{
public class A : MonoBehaviour {
private void Awake()
{
// it is save to remove a callback first even
// if it was not added yet. This makes sure it is
// added only once always
AEventHandler.OnIvokeA -= SomePrivateMethod;
AEventHandler.OnIvokeA += SomePrivateMethod;
}
private void OnDestroy()
{
AEventHandler.OnIvokeA -= SomePrivateMethod;
}
private void SomePrivateMethod()
{
// magic
}
}
}
Now in B you would rather simply do
// invoke the event and whoever is added as listener will do
// whatever he registered
// in this case all A instances execute their method
AEventHandler.InvokeAEvent();
Unity Event
If you have however only one class A which throws an event and you want others to react to it simply use a UnityEvent like
public class A : MonoBehaviour
{
public UnityEvent OnSomethingHappened = new UnityEvent();
private void SomeMethodIWillRun()
{
//...
OnSomethingHappened.Invoke();
}
}
Now you cann easily add callbacks to that event in the Unity Inspector by dragging in GameObjects/Components and select the method to call. (Exactly the same thing is used for the onClick event of the Button component btw. ;) )
And you could still add callbacks via script on runtime like
public class B : MonoBehaviour
{
public A referenceToA;
private void Start()
{
referenceToA.OnSomethingHappened.AddCallback(OnASomethingHappened);
}
private void OnDestroy()
{
referenceToA.OnSomethingHappened.RemoveCallback(OnASomethingHappened)
}
private void OnASomethingHappened()
{
//
}
}
Note: Typed on smartphone so no warranty but I hope the idea gets clear.