TextMeshPro & Addressables (Asset Bundles)

There was a discussion about this on Addressables forum, but since it’s been 2 years since last official response, I’d like to get some recent insights into the situation.

Our project stumbled upon this issue because we made the decision to put almost all of the game assets into Addressables (and away from Resources/ BuildSettings) except for a small Boot scene. We have a lot of UI prefabs/scenes with TMP elements that reference materials and font assets. These same assets are meant to be in Resources folder by the way TextMeshPro is currently implemented (as of 3.0.6).

Because there is no way to have Unity reference assets across Resources and AssetBundles, there is inevitable asset duplication. Because we target mobile platforms and some font assets were pretty big, this became an issue. To overcome, I had to resort to some ugly hacking (see thread above). I had to put everything (TMP Settings, materials, Font Assets, Sprites) outside of Resources - it’s the only way to get rid of duplication.

But then I realized just how deeply TextMeshPro is tied to Resources folder both at Editor and Runtime:

  • TMP Settings must be in a strict hard-coded path in Assets folder (not ideal because we try to put all third party stuff into a dedicated folder…) and must be in Resources. You can ignore it and just move this asset out and load via Addressables, but TMP will complain that “essential assets” needs to be imported and some editor functions won’t work (like font asset creation).

  • You can’t dynamically load fallback fonts. This is a big problem if you want to support various languages but don’t want to keep ALL fonts in memory (some fonts like Japanese or Chinese have thousands of symbols and better not be loaded until they are needed). To work around this I had to:

  • Create a “template” TMP Settings asset with empty Fallback fonts array;

  • Load this asset via Addressables and then manually inject (by reflection hacking) only the required Font Assets based on current user language into the instantiated clone of the asset;

  • Replace the static instance of TMP Settings with this clone (again, using reflection).

  • If you reference materials by name using rich text tags and your materials are not in Resources folder - it won’t work. Some more hacking is required.

  • Doing a search in code for Resources.Load you can see some even less obvious uses. Each one is a potential issue…

So in the end, current situation with TextMeshPro assets is rather grim:

  • TextMeshPro is a must-have package promoted by Unity;
  • Addressables is a must-have package promoted by Unity;
  • They do “kinda” work together… except not really, you get a duplicate asset issues which can be (and actually is) critical for “real” production projects on mobile platform;
  • TMP is hard-wired into Resources (which Unity manual advises against using since forever…) which does not work at all with Asset Bundles;
  • If you need to optimize text asset memory usage / build size - you need to resort to hacking or creating and maintaining local fork of TextMeshPro (both are a lot of work and have pros and cons).

Please consider addressing this.

14 Likes

I stumble on the same issue in our project. Due to the usage of adressables, we have multiple font texture/material duplicated in memory.
Since we are running short in term of memory, it would have been great to have some news from Unity about this subject.

1 Like

Same.
Even if they can’t fix this, a guide or suggested how-to do it ourselves will be a tremendous helpful

1 Like

We are also waiting for solutions for this.

Hope this gets addressed soon. This is a huge deal for multi language support on a handheld, wasting 120+ MB of RAM.

Please do something about this. The silence is deafening

4 Likes

It says a lot that a detailed post about a critical issue like this hasn’t received a single response from Unity in over an year.

Memory is a scarce resource on some platforms and this is wasting 100MB+ for unused fonts just in the rare occasion a fallback is necessary.

As @phobos2077 mentioned, both packages are pretty much a must-have on any professional project, and getting them to properly work together involves a lot of work.

8475959--1126610--upload_2022-9-29_12-13-0.png

3 Likes

waiting for so long

2 Likes

With the advent of WaitForCompletion from quite a while ago, replacing all the Resources.Load calls in TMP with Addressables isn’t too bad.

Obviously this requires keeping a local copy of the TMP package but it’s not a terribly involved process. I came across this post so I spent a little bit of time figuring out a nice way to do it. Basically, I have it set up a little bit like the way the localization package works.

With this setup, I replaced most (there are a few stragglers in my test project) of the Resources.LoadAsset with calls to this class:

using UnityEngine;

#if TMP_ADDRESSABLES
using UnityEngine.AddressableAssets;
#endif

namespace TMPro
{
    public static class TMP_ResourceLoader
    {
        public static TMP_Settings LoadSettings()
        {
            TMP_Settings result = null;
#if TMP_ADDRESSABLES
            result = Addressables.LoadAssetAsync<TMP_Settings>( $"TMP-Settings" ).WaitForCompletion();
#else
            result = Resources.Load<TMP_Settings>("TMP Settings");
#endif
            return result;
        }

        public static TMP_FontAsset LoadFontAsset( string name )
        {
            TMP_FontAsset result = null;
#if TMP_ADDRESSABLES
            result = Addressables.LoadAssetAsync<TMP_FontAsset>( $"TMP-Font-{name}" ).WaitForCompletion();
#else
            result = Resources.Load<TMP_FontAsset>( TMP_Settings.defaultFontAssetPath + name );
#endif
            return result;
        }

        public static Material LoadMaterial( string name )
        {
            Material result = null;
#if TMP_ADDRESSABLES
            result = Addressables.LoadAssetAsync<Material>( $"TMP-Material-{name}" ).WaitForCompletion();
#else
            result = Resources.Load<Material>( TMP_Settings.defaultFontAssetPath + name );
#endif
            return result;
        }

        public static TMP_ColorGradient LoadColorGradient( string name )
        {
            TMP_ColorGradient result = null;
#if TMP_ADDRESSABLES
            result = Addressables.LoadAssetAsync<TMP_ColorGradient>( $"TMP-Gradient-{name}" ).WaitForCompletion();
#else
            result = Resources.Load<TMP_ColorGradient>( TMP_Settings.defaultColorGradientPresetsPath + name );
#endif
            return result;
        }

        public static TMP_SpriteAsset LoadSpriteAsset( string name )
        {
            TMP_SpriteAsset result = null;
#if TMP_ADDRESSABLES
            result = Addressables.LoadAssetAsync<TMP_SpriteAsset>( $"TMP-Sprite-{name}" ).WaitForCompletion();
#else
            result = Resources.Load<TMP_SpriteAsset>( TMP_Settings.defaultSpriteAssetPath + name );
#endif
            return result;
        }
    }
}

The results are pretty promising in that I can still have a default font set up and working correctly. You’d still have to be careful that you don’t accidentally touch something TMP related until your bundles are up and running, and you could of course run in to hiccups because none of this is done asynchronously.

I would really like to see Unity address this issue as well - but in the meantime this might be a path for others to explore.

7 Likes

I am also running into this issue, where all these TMP assets are being duplicated

Stumbled upon this problem. Can we have some response from Unity? @Stephan_B ?

1 Like

OMG. This is so annoying!
@ If you implement new features and try to make them the new standard, please make sure they work together with other standard systems!

It’s always the same problem: Whenever a new feature is added to Unity (like the Addressables system) and promoted as the way to go, it causes a lot of trouble because the other systems are not meant to work the way the new system requires them to do. This makes the feature (and therefore Unity) not suitable for production. Eventually some years in the future the feature will work as intended. But this is the time it will for sure be replaced by another feature facing the same problems.

In the end the problem is that teams at Unity work on different features and it seems that they work somewhat in isolation. Please, Unity: Make sure to only release a feature and system when it is actually ready to be used with all the other features and systems. You cannot simply add something and ignore that all the other systems have to be adjusted to the new feature.

4 Likes

argh, so this is the problem with creating TMP Material Presets via scripts and then using AssetDatabase.SaveAssets to Resources folders in later versions of Unity.

Great… fixing when?

Also running into TMP and Addressables issues, but looks like TMP development is being frozen whilst its integrated into the UGUI and UITK packages in various ways, so I wouldn’t expect any fixes anytime soon.

1 Like

I was fighting with this issue just now. My solution was to nullify m_defaultFontAsset field in the TMP Settings.asset for the duration of the build. This stops the asset, which is in the Resources folder, from duplicating your addressable fonts in the build.

Here’s a helper class for this:

public class BuildContext : System.IDisposable
{
    public BuildContext(string buildVersion = null)
    {
        if (buildVersion != null) {
            PlayerSettings.bundleVersion = buildVersion;
        }

        Provider.Checkout(Provider.GetAssetByPath("Assets/TextMesh Pro/Resources/TMP Settings.asset"), CheckoutMode.Asset);
        SetFieldValue(TMP_Settings.instance, "m_defaultFontAsset", null);
    }
  
    public void Dispose()
    {
        Provider.Revert(Provider.GetAssetByPath("ProjectSettings/ProjectSettings.asset"), RevertMode.Normal);
        Provider.Revert(Provider.GetAssetByPath("Assets/TextMesh Pro/Resources/TMP Settings.asset"), RevertMode.Normal);
    }

    private static void SetFieldValue(Object obj, string fieldName, object value)
    {
        FieldInfo field = (FieldInfo)obj.GetType().FindMembers(MemberTypes.Field, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, (member, _) => member.Name == fieldName, null)[0];
        field.SetValue(obj, value);
        EditorUtility.SetDirty(obj);
        AssetDatabase.SaveAssetIfDirty(obj);
    }
}

As a bonus, it sets and reverts the project build version also.

1 Like

@uDamian Can you weigh in on this?

^ this issue hasn’t had an official response in 4 years don’t know why they’d start now.

1 Like

Omg, also waiting for a solution…

2 Likes

Not only some builds takes 50mins to complete with those milions of Shaders Variantes. Every time it fails, is because of an unity related issue and we never have a way out.

It makes me wonder why haven’t I switched to UE5 yet.

1 Like

Even just acknowledgement at this point would be nice.

I wouldn’t mind giving that IDisposable BuildContext object above a try just to keep things quiet without invalidating packages, but I don’t really know where that script would go or how to reference it. I guess I’d probably make child classes of the default build scripts or something? I can’t say I’ve had to do something like this yet.