We have some tables that are loaded when the game initializes (I have tried with both PreloadTables and GetTableAsync, but I’d prefer to use PreloadTables). They contain data that is expected to be always available right away synchronously.
Later in the game, other code can temporarily use GetTableAsync to show some info in a screen, and ReleaseTable when the screen closes. Any of the tables can already have been loaded at initialization.
The problem is that ReleaseTable seems to unload the table regardless of whether other code also had loaded it, it doesn’t seem to maintain a reference count by itself or make addressables do it. We want to get the localizations synchronously, and it’s not an issue most of the time, but now we need to make a WebGL version so the table needs to stay loaded to work.
Is there a way to “acquire” the tables loaded during initialization, so other code can load and unload them without having to worry, or does our code have to keep track of that itself? I have tried Addressables.ResourceManager.Acquire on the handles returned by PreloadTables and GetTableAsync, but that doesn’t work.
Please make sure you are using the latest version 1.4.4 as this has some related fixes. If its not visible in the package manager you can edit the manifest.json file in the packages folder.
GetTableAsync and PreloadTables do not complete immediately, they make a request that will be completed asynchronously. They can be forced to complete immediately by calling WaitForCompletion on the operation however this is not supported on WebGL.
If you want to hold onto an operation you would typically call Acquire on it and then call Release when you are finished. Failure to call Release can result in the asset never being unloaded and taking up unnecessary memory and failure to call Acquire and cause the asset to be unloaded whilst you are still using it.
If this is not working for the table and preload operation then it could be a bug, could you please file a bug report for that? Calling Acquire on the GetTable operation should keep the table alive. Can you share the code?
I have tested with Localization 1.4.4 and Addressables 1.21.15, on Unity 2021.3.25. This is some code with that behaviour:
TableReference tableReference = "Test table";
void CheckIfTableLoaded() => Debug.Log(LocalizationSettings.StringDatabase.IsTableLoaded(tableReference) ? "Is loaded" : "<color=orange>Is NOT loaded</color>");
StartCoroutine(Coroutine());
IEnumerator Coroutine()
{
// Wait for localization to initialize:
var localizationOper = LocalizationSettings.InitializationOperation;
if (!localizationOper.IsDone)
yield return localizationOper;
if (localizationOper.Status != AsyncOperationStatus.Succeeded)
{
Debug.LogError("Failed initializing localization");
yield break;
}
Debug.Log("Localization initialized");
CheckIfTableLoaded();
// Load the table for the first time:
var firstTableLoadOper = LocalizationSettings.StringDatabase.GetTableAsync(tableReference);
if (!firstTableLoadOper.IsDone)
yield return firstTableLoadOper;
if (firstTableLoadOper.Status != AsyncOperationStatus.Succeeded)
{
Debug.LogError("The first table load operation failed");
yield break;
}
Debug.Log("The first load operation succeeded");
CheckIfTableLoaded();
// Acquire the operation:
Addressables.ResourceManager.Acquire(firstTableLoadOper);
Debug.Log("Operation acquired");
CheckIfTableLoaded();
// Load the table for the second time:
var secondTableLoadOper = LocalizationSettings.StringDatabase.GetTableAsync(tableReference);
if (!secondTableLoadOper.IsDone)
yield return secondTableLoadOper;
if (secondTableLoadOper.Status != AsyncOperationStatus.Succeeded)
{
Debug.LogError("The second table load operation failed");
yield break;
}
Debug.Log("The second load operation succeeded");
CheckIfTableLoaded();
// Release the table:
LocalizationSettings.StringDatabase.ReleaseTable(tableReference);
Debug.Log("Table released");
CheckIfTableLoaded();
}
In the console I get these log entries. Unless I made some mistake in the code, I’d expect the last entry to be “Is loaded”, as I acquire the first load operation, is that right?
Localization initialized
Is NOT loaded
The first load operation succeeded
Is loaded
Operation acquired
Is loaded
The second load operation succeeded
Is loaded
Table released
Is NOT loaded
Checking the addressables event viewer, I notice there are 2 entries lingering after everything finishes:
So I assume “Test table_en.asset” is still around, but localization doesn’t “know” about it? Or maybe some other needed asset was released (like the shared table data)?
IsTableLoaded will return false if the database has released the table or has not loaded it yet, the actual table may still be loaded though if addressables still has a reference to it. The next time you request the table it will acquire the table. IsTableLoaded is really just checking if we have cached the table operation locally.
So the method name is a bit misleading as it doesn’t guarantee the table is not being held somewhere else such as in your example.
Feel free to file a bug report, there are probably some improvements we could make here.
The problem is that, after the code I posted executes and IsTableLoaded returns false, if now I try to get a localized string synchronously (and I assume an asset as well) with LocalizedString.GetLocalizedString, Localization starts a new table load operation that sometimes (see below) is not finished, so that fails in WebGL.
I was going a bit crazy because I wasn’t able to reproduce that behaviour on the test code, but it always happened in the full project, and turns out that using a LocalizedString that has a TableReference created with the table name everything works fine, Localization creates a table loading operation which Addressables returns as finished; but when using a table GUID the behaviour is different, and the loading operation Localization creates is not finished. I haven’t investigated more about it, but I guess it may have something to do with “LocalizedDatabase.cs” branching on line 367 depending on whether the table ReferenceType is a GUID or not.
// This causes a "WaitForCompletion" call on an operation that isn't finished,
// and blocks on WebGL:
TableReference tableReference = new Guid("c069d1a8b2c83004f9b83e437cbbd62d");
LocalizedString localizedString = new LocalizedString(tableReference, "Entry 1");
···
localizedString.GetLocalizedString();
// This doesn't:
TableReference tableReference = "Test table";
LocalizedString localizedString = new LocalizedString(tableReference, "Entry 1");
···
localizedString.GetLocalizedString();
My assumption would be that both should have the same behaviour, or is there something I’m missing?
When using a guid we have to perform an extra step to first load the shared table data in order to get the table collection name which is then used to load the table. So this is likely what you are seeing. The database will have an entry for both table name and guid which use the same table operation.
Try calling GetLocalizedStringAsync. If you are using a LocalizedString then use the StringChaged event and it will handle all the async loading for you. Marking the tables as preload and using StringChanged will probably produce the best results.
Getting the localizations asynchronously is what I’m trying to avoid. As a workaround, I keep track of whether the table was already loaded at the start and in that case I don’t load and unload it later, but as a suggestion, wouldn’t it be possible for Localization to always load the shared table data regardless of whether a GUID or a table name is used, and make the operation handle returned by PreloadTables and GetTableAsync include the shared data so that it could be acquired? It would make the behaviour more consistent and predictable.
We do load the shared table data for both approaches however for guid we have to load it first whereas with name it gets loaded as part of the table and we can grab it later and record it for reuse.
Can you not mark all your tables as preload and load them all at the start? You won’t need to keep track of them then.
I can mark them as preload, I was just trying to manage memory a bit. But maybe it’s not worth the hassle for the tables and I’ll just do it for the localized assets.
That sounds better. Try using the memory profiler and see how much memory the tables are taking, compared to everything else (texture, audio etc) it will be very small and in most cases not worth the effort to unload them. Asset tables do give you control over loading and unloading each asset and will benefit from unloading assets you are not using any more. You still need to manage the references, so acquire any asset you want to hold on to and release it when you are finished. You will also need to tell the asset table to release its reference.