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.
Related
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;
}
}
I'm working on a game in Unity, using Firebase for user and game data.
Everything is fine saving the game to Unity, but in Multiplayer I have the challenge to pair an open game to a unique player 2.
The flow is:
1. You look for a game with a state of "open"
2. If no game is found within X seconds, a new one is created with the state of "open"
I have a query that returns open games (those with state "open")
This query is added a Firebase eventhandler
public void OnButtonLookForOpenMPGames()
{
this.OpenFirebaseGamesAsHost = this.LiveGamereference.OrderByChild("State").EqualTo(WYMSettings.MP_GAME_STATE_OPEN_TO_PLAYER2)
.LimitToFirst(3);
this.OpenFirebaseGamesAsHost.ChildAdded += this.OnOpenMPGameFound;
}
This fires fine in the handler:
private void OnOpenMPGameFound(object sender, ChildChangedEventArgs args)
{
Debug.Log("Looking for open games");
if (args.DatabaseError != null)
{
// handle errors
}
else
{
Debug.Log("Open game found (it may be my own)");
// remove event listener because we got a hit
// it will fire as many times as it's true!
this.OpenFirebaseGamesAsHost.ChildAdded -= this.OnOpenMPGameFound;
if (args.Snapshot.Child("HostId").Value.ToString() != FirebaseAuth.DefaultInstance.CurrentUser.UserId)
{
Debug.Log("Other game than mine found open");
// this.AddDelayedUpdateAction(() =>
// {
// this.MPGameLockInTransaction(args.Snapshot.Reference);
// });
this.MPGameLockInTransaction(args.Snapshot.Reference);
}
}
}
The problem: The handler passes this on to the transaction function, but it fails with an inner exception (Internal Task Faulted) - I've tried many different variations, but can't figure this one out from the official Firebase example.
Problem code:
What I want to achieve is to lock only that game to those two players, the host and the players 2 in this example querying for open games. The State is an int, but here a string for clarity.
private void MPGameLockInTransaction(DatabaseReference mpreference)
{
mpreference.RunTransaction(mutableData =>
{
MultiPlayerGame transactionMPG = mutableData.Value as MultiPlayerGame;
if (transactionMPG == null)
{
return TransactionResult.Abort();
}
if (transactionMPG.State != "open")
{
// game is taken, abort
Debug.Log("transaction aborted");
return TransactionResult.Abort();
}
transactionMPG.State = "game started";
mutableData.Value = transactionMPG;
return TransactionResult.Success(mutableData);
}).ContinueWith(task =>
{
if (task.Exception != null)
{
Debug.Log("Transactionlock to game failed" + task.Exception.ToString());
// Look over again
// not implemented yet
}
else
{
Debug.Log("starting game immediately");
this.AddDelayedUpdateAction(() => this.StartMPGameImmediatelyFromSearch());
}
});
}
This is just a guess but it might be related to threading.
In newer .Net version ContinueWith might end on a thread where most of the Unity API may not be called (including Debug.Log).
Instead you could try and use ContinueWithOnMainThread from the extensions instead which was created exactly for this reason.
With the .NET 4.x runtime, continuations may often execute in the background, whilst this extension method pushes them onto the main thread.
The .NET 3.x runtime didn't have this issue as continuations were contained within the Parse library, which did this implicitly.
I have an array of prefabs i want to show a preview of in my custom editor. This works for gameobjects with a mesh renderer, for example the basic quad. However when i try to use AssetPreview.GetAssetPreview(tilePrefab.gameObject); on gameobject with a UnityEngine.UI.Image and a canvas renderer it always returns null.
Below is the part of the code that draws the previews.
public class MapEditor : Editor
{
public override void OnInspectorGUI()
{
for (int prefabIndex = 0; prefabIndex < TileSet.TilePrefabs.Count; prefabIndex++)
DrawIconTexture(prefabIndex, columnCount);
}
private void DrawIconTexture(int prefabIndex, int columnCount)
{
TileBehaviour tilePrefab = TileSet.TilePrefabs[prefabIndex];
Texture iconTexture = AssetPreview.GetAssetPreview(tilePrefab.gameObject);
Rect iconRect = GetIconRect(prefabIndex, columnCount);
if (iconTexture != null)
GUI.DrawTexture(iconRect, iconTexture);
else if (AssetPreview.IsLoadingAssetPreview(tilePrefab.gameObject.GetInstanceID()))
Repaint();
}
}
I know GetAssetPreview loads assets async, that is solved by the repaint. I have also tried
while(iconTexture == null)
iconTexture = AssetPreview.GetAssetPreview(tilePrefab.gameObject);
But that never finishes.
I also tried to use the texture of the Image
if (iconTexture == null)
iconTexture = tilePrefab.GetComponent<Image>().sprite.texture;
But that does not work because the sprite is in an atlas and all of the atlas is shown.
Edit: misread the question. I actually tried to use IsLoadingAssetPreview and IsLoadingAssetPreviews for few hours, without success. I ended up using a sad little trick
if (_previews.All(pair => pair.Value != null)) return;
_previews = GeneratePreviews();
I put that in the Update() loop of my EditorWindow. It's pretty hacky, I'm going to ask Unity if the AssetPreview methods are actually working.
Old answer, irrelevant:
You are not using the while loop and GetAssetPreview correctly.
GetAssetPreview will launch an asynchronous loading of the preview. To know if the preview is fully loaded you need to call AssetPreview.IsLoadingAssetPreview.
A pretty simple and brutal way of doing it (it will block execution) is :
var preview = AssetPreview.GetAssetPreview(item);
while (AssetPreview.IsLoadingAssetPreview(item.GetInstanceID())) {
// Loading
}
As usual, careful with while loops. Note that there is also a AssetPreview.IsLoadingAssetPreviews method with no parameters.
you should just use "using UnityEditor"
Hi I'm struggling with this issue.I searched a lot before asking here.
So i have programmed an arcade game which when it dies it pops up a menu with a continue option using one life out of five.Like Subway Surfers with the keys.
The issue is that when i use one heart i want to save it.But everytime i restart the game it keeps saying the initial number which is five.Or without using a life,it by itself,subtracts by one without even clicking the "Save me" button.
Here is my code:
public Text heart;
public Text heart2;
public int counter;
void Start () {
heart.text = "" + counter;
heart2.text = "" +counter;
}
// Update is called once per frame
void Update () {
}
public void SaveMe()
{
heart.text = counter.ToString();
heart2.text = counter.ToString();
int heartScore = PlayerPrefs.GetInt("Life2", 0);
heartScore--;
if(heartScore<0)
{
heartScore = counter;
}
PlayerPrefs.SetInt("Life2", heartScore);
heart.text = PlayerPrefs.GetInt("Life2", 0).ToString();
heart2.text = PlayerPrefs.GetInt("Life2", 0).ToString();
}
}
Then i call this method in the button script.
You should need to save life counter in a file or PlayerPrefs in unity so that loading a scene doesn't effect the counters.
Stores and accesses player preferences between game sessions.
When Unity loads a new scene (if not in an additive way) it will destroy everything that belongs to it before loading the new one. If you don't want a GameObject to be destroyed automatically, read the instructions for DontDestroyOnLoad
Also consider generally to separate your game model (in this occasion the variable counter) to a different Component and use scriptable objects for managing it/them. It will make your life easier in the long run.
I don't know what is wrong with my destroy portion of the below code. I'm trying to have only 1 GameControl persist throughout the scenes. It seems that every time I switch back to this scene, my GameControl is read as null and a new GameControl is generated and I end up having more than 1 GameControl persisting.
For reference: I tried to port the code from 18:24 to JavaScript from this link
Please advise.
#pragma strict
var control : GameControl;
function Awake () {
Debug.Log("GameControl runs");
if (control == null)
{
DontDestroyOnLoad(gameObject);
control = this;
Debug.Log(control);
} else if (control != this)
{
Destroy(gameObject);
Debug.Log("Destroy?");
}
}
function Update ()
{
}
The problem is that you're not using a static variable like this:
static var control : GameControl;
In simple terms being static means this variable will hold the same value across all your scripts thus the check if(control == null) will always remain true because your GameObject will create a new variable when you load a new scene. This new object is still not static and is therefore completely different. I hope this helps. If you are still confused I'd advise reading some single pattern articles because there are many ways to achieve it other than this.