Reputation: 950
I'm working on a system to save game state in Unity. The current structure looks like this:
public void SaveGame() {
StopAllCoroutines();
if (source != null)
{
source.Cancel();
}
StartCoroutine(DoSave());
}
protected IEnumerator DoSave(){
/// Code that prepares a bunch of stuff necessary to save the game goes here
foreach (item in itemsToSave)
{
// Code that adds 'item' to a data structure in memory that is accessible by later functions goes here //
yield return new WaitForSeconds(0.05f);
}
source = new CancellationTokenSource();
WriteFiles(source.Token);
}
private async Task WriteFiles(CancellationToken token)
{
Task task = Task.Run(() => GenerateSaveFiles(), token);
// (A canceled task will raise an exception when awaited).
await task;
}
private void GenerateSaveFiles()
{
/// Functions that actually write to disk go here
/// E.g. File.WriteAllText();
/// This function has access to the place in memory where 'items' were being added to in the DoSave() foreach loop, so it just dumps that memory storage to disk
}
I set it up this way so I can save asynchronously in a separate thread and track the status of it (e.g. if it fails or not, and error messages).
SaveGame()
is then called around my code base whenever different events (like picking up items, killing enemies etc) to save the game in real-time. This mostly works well for 99% of my use-cases but there are instances where I want to call SavGame()
and wait for it to finish before doing anything else at all. For example, if a player dies, I want to write the state to disk without giving a chance for anything else (e.g. respawning the player in a different scene) to occur.
I have looked into a few things like callbacks (e.g. https://codesaying.com/action-callback-in-unity/) but I'm not sure how to correctly set this up for my use-case of mostly having SaveGame()
tasks run async but other times being able to wait for it to complete, or at a minimum having the method that called SaveGame()
knowing when it has completed.
Upvotes: 1
Views: 2536
Reputation: 950
Ok, after a lot of reading and effort I was able to get this to work. There's a lot of things that can get in the way because of my setup of using Coroutine but also a Task and other Coroutines nested within.
My solution was two-fold
DoSave()
) and any other Coroutines nested within I pass a parameter waitForSaveComplete
and if true, I do not run any of the yield statements in DoSave()
or its nested coroutines. So from the example in my question, I wrapped all my yield statements like so:if (!waitForSaveComplete)
{
yield return new WaitForSeconds(0.05f);
}
I also had some synchronized nested Coroutines that whose yield statements I wrapped in the same way.
Next, I changed the calls of nested coroutines from:
yield return StartCoroutine(MySynchronizedCoroutine());
to:
if(!waitForSaveComplete) {
yield return StartCoroutine(MySynchronizedCoroutine(waitForSavComplete));
} else
{
// IMPORTANT: Make sure MySynchronizedCoroutine also uses
// waitForSaveComplete to wrap its own yield statements
// or else this won't work!!!
StartCoroutine(MySynchronizedCoroutine(waitForSavComplete));
}
Wrapping the yield statements like this works because that's where the Coroutine pauses and gives up control until the corresponding Wait
condition is met. Up until that point it is 'blocking' and given that I'm not sure why it took me so long to think of this.
WaitUntil
in the async case to make sure it completes before anything else happens within the Coroutine itself. In the waitForSaveComplete
case I simply did not even start a Task, instead running GenerateSaveFiles()
directly like soif (waitForSaveComplete)
{
GenerateSaveFiles();
} else
{
Task writeTask = WriteFiles(source.Token);
yield return new WaitUntil(() => writeTask.IsCompleted);
}
Again, another 'duh' moment, just don't like.. start a task in cases where it's not needed.
This seems to work but also kind of complicated because of the bit with the nested coroutines which seems like quite the fertile ground for horrific, hard to track bugs. Alas my async programming experience is severely limited so I don't have any better design or implementation ideas.
Upvotes: 1
Reputation: 4056
Just do a simple Boolean property that anyone can access:
public bool IsSavingGame {
get; private set;
}
// ...
private IEnumerator SaveGameCoroutine(){
IsSavingGame = true;
// Your save functionality.
IsSavingGame = false;
}
For example, if you want deny any loading of scene while it's saving, you can write up this script:
// or you can singleton it
[SerializeField]
private SaveManager yourSaveManger
public void LoadScene(){
StartCoroutine(LoadSceneCoroutine());
}
private IEnumerator LoadSceneCoroutine(){
while (yourSaveManger.IsSavingGame){
// Show a loading or saving screen or something
yield return null;
}
SceneManager.LoadScene("Your Scene Name");
}
Upvotes: 1