Author Topic: Converting existing UnityEngine.UI.Text objects into TextMeshProUGUI  (Read 8064 times)

guillaumeportes

  • Newbie
  • *
  • Posts: 2
I recently switched our game to using TMPro, at a very late stage of development. It's a CCG / RPG scheduled to ship in the next couple of months, so as you can imagine we had a lot of text components (mostly in prefabs, but also in some scenes). The main reason we switched to TMPro is performance, as a recent switch to Unity 5.2 (from 4.6) had a huge negative impact, with some noticeable spikes coming from Text. Because of that we needed to convert all existing assets to using TMPro.
As it's a pretty non straight forward process I thought I would share it here.

Converting a Text object to TMPro is fairly straight forward (and I suspect our method is not "correct" but it gave us the desired results for the features we used). The tricky bit is to convert those objects in place, i.e. have references to the old Text object reference the new TMPro object.

Of course make sure you have your project in source control before attempting such a conversion job!

1) Create a base text class. In order to convert in place you need to have both classes (the one you're converting from and the one you're converting to) inherit from the same base class, as otherwise changing the reference type will invalidate (null) all existing references.
In our case, you need to clone the UI repository (https://bitbucket.org/Unity-Technologies/ui) and in Text.cs, add the following:

Code: [Select]
public abstract class TextProxy : MaskableGraphic
{
    public abstract string text { get; set; }
}

Also change Text to inherit from TextProxy.
Build the solution and replace the Unity UI DLLs with those ones (the ones used by Unity - on OSX - are located in /PATH_TO_UNITY/Unity.app/Contents/UnityExtensions/Unity/GUISystem and sub folders).

2) Replace all references to UnityEngine.UI.Text in your code by UnityEngine.UI.TextProxy, including serialized members as well as non serialized ones.
Make TextMeshProUGUI inherit from TextProxy.

3) We then need a conversion class in order to preserve the right attributes.

Code: [Select]
class TextWrapper
{
public string text;
public TextAnchor alignment;
public int fontSize;
public FontStyle fontStyle;
public HorizontalWrapMode horizontalOverflow;
public float lineSpacing;
public bool resizeTextForBestFit;
public int resizeTextMaxSize;
public int resizeTextMinSize;
public bool supportRichText;
public VerticalWrapMode verticalOverflow;
public Color color;

static TextMeshProFont font = null;
static TextMeshProFont Font
{
get
{
if (font == null)
{
font = AssetDatabase.LoadAssetAtPath<TextMeshProFont>("Assets/Fonts/Candarab SDF.asset");
}

return font;
}
}

public TextWrapper(Text t)
{
text = t.text;
alignment = t.alignment;
fontSize = t.fontSize;
fontStyle = t.fontStyle;
horizontalOverflow = t.horizontalOverflow;
lineSpacing = t.lineSpacing;
resizeTextForBestFit = t.resizeTextForBestFit;
resizeTextMaxSize = t.resizeTextMaxSize;
resizeTextMinSize = t.resizeTextMinSize;
supportRichText = t.supportRichText;
verticalOverflow = t.verticalOverflow;
color = t.color;
}

public void ProcessTMPro(TextMeshProUGUI t)
{
t.text = text;
t.font = Font;
t.fontSize = fontSize;
t.lineSpacing = lineSpacing;
t.richText = supportRichText;
t.enableAutoSizing = resizeTextForBestFit;
t.fontSizeMin = resizeTextMinSize;
t.fontSizeMax = resizeTextMaxSize;
t.color = color;

if (horizontalOverflow == HorizontalWrapMode.Wrap)
{
t.enableWordWrapping = true;
}
switch (alignment)
{
case TextAnchor.UpperLeft:
{
t.alignment = TextAlignmentOptions.TopLeft;
break;
}
case TextAnchor.UpperCenter:
{
t.alignment = TextAlignmentOptions.Top;
break;
}
case TextAnchor.UpperRight:
{
t.alignment = TextAlignmentOptions.TopRight;
break;
}
case TextAnchor.MiddleLeft:
{
t.alignment = TextAlignmentOptions.Left;
break;
}
case TextAnchor.MiddleCenter:
{
t.alignment = TextAlignmentOptions.Center;
break;
}
case TextAnchor.MiddleRight:
{
t.alignment = TextAlignmentOptions.Right;
break;
}
case TextAnchor.LowerLeft:
{
t.alignment = TextAlignmentOptions.BottomLeft;
break;
}
case TextAnchor.LowerCenter:
{
t.alignment = TextAlignmentOptions.Bottom;
break;
}
case TextAnchor.LowerRight:
{
t.alignment = TextAlignmentOptions.BottomRight;
break;
}
}
}
}

The above doesn't deal with outlines (I'll show how we dealt with those below). It of course relies on having a TMPro font somewhere in your project. In our game 99% of Text objects use the same one so we didn't bother with anything clever.

4) Then the following code deals with converting a prefab.

Code: [Select]
static void ConvertPrefabToTMPro(GameObject prefab)
{
Debug.Log("Converting prefab at " + AssetDatabase.GetAssetPath(prefab));
var references = new Dictionary<Text, Dictionary<Component, HashSet<string>>>();
bool dirty = false;
foreach (var t in prefab.GetComponentsInChildren<Text>(true))
{
references[t] = new Dictionary<Component, HashSet<string>>();
}
foreach (var c in prefab.GetComponentsInChildren<Component>(true))
{
if (c == null)
{
continue;
}

SerializedObject so = new SerializedObject(c);
var sp = so.GetIterator();
while (sp.NextVisible(true))
{
if (sp.propertyType == SerializedPropertyType.ObjectReference && sp.objectReferenceValue != null)
{
var t = sp.objectReferenceValue as Text;
if (t != null)
{
if (! references[t].ContainsKey(c))
{
references[t][c] = new HashSet<string>();
}
references[t][c].Add(sp.propertyPath);
}
}
}
}
foreach (var t in references.Keys)
{
dirty = true;

var convert = new TextWrapper(t);
var go = t.gameObject;
Object.DestroyImmediate(t, true);
var tmpro = go.AddComponent<TextMeshProUGUI>();
convert.ProcessTMPro(tmpro);

foreach (var c in references[t].Keys)
{
foreach (var n in references[t][c])
{
Debug.Log(n);
SerializedObject so = new SerializedObject(c);
var sp = so.FindProperty(n);
if (sp == null)
{
Debug.LogError("Can't find property " + n + " in " + c);
}
sp.objectReferenceValue = tmpro;
sp.serializedObject.ApplyModifiedPropertiesWithoutUndo();
}
}
}

if (dirty)
{
EditorUtility.SetDirty(prefab);
}
}

The above does the following steps:
  • Gather Text objects
  • Gather references to Text objects
  • Convert Text objects (and destroy them) to TMPro ones
  • Fix up references so the new TMPro objects are referenced

You can easily gather all objects in a folder / scene with the following:

Code: [Select]
[MenuItem("Assets/Convert to TMPro")]
public static void ConvertToTMPro()
{
var gos = new List<GameObject>();

if (Selection.activeObject as GameObject != null)
{
gos.Add(Selection.activeObject as GameObject);
}
else
{
string path = AssetDatabase.GetAssetPath (Selection.activeObject);
Debug.Log(path);
if (path == "")
{
path = "Assets";
}
else if (Path.GetExtension (path) != "")
{
path = path.Replace(Path.GetFileName(AssetDatabase.GetAssetPath(Selection.activeObject)), "");
}

gos = PrefabTools.GetPrefabsAtPath(path);
}

foreach (var go in gos)
{
ConvertPrefabToTMPro(go);
}
}

public static List<GameObject> GetPrefabsAtPath(string path)
{
var paths = new string[1];
paths[0] = path;
var guids = AssetDatabase.FindAssets("", paths);
var results = new List<GameObject>();
foreach (var guid in guids)
{
var assets = AssetDatabase.LoadAllAssetsAtPath(AssetDatabase.GUIDToAssetPath(guid));
foreach (var asset in assets)
{
var g = asset as GameObject;
if (g != null)
{
if (AssetDatabase.IsMainAsset(asset))
{
results.Add(g);
}
}
}
}

return results.Distinct().ToList();
}

The above will not deal with references to objects outside of a prefab it converts, so be mindful of that as you'll have to fix them up manually. In our game that was the case for maybe 0.1% of prefabs and a few scene objects.

6) Finally we needed to deal with Outline. We looked at setting stuff dynamically but decided against it, as we have very few different types of outlines (most of them are black) so we simply created a new font material per outline color (see TMPro's excellent tutorial videos for how to do that), and converted the old objects with the following:

Code: [Select]
[MenuItem("Assets/Convert outlines")]
public static void ConvertOutlines()
{
var gos = new List<GameObject>();

if (Selection.activeObject as GameObject != null)
{
gos.Add(Selection.activeObject as GameObject);
}
else
{
string path = AssetDatabase.GetAssetPath (Selection.activeObject);
Debug.Log(path);
if (path == "")
{
path = "Assets";
}
else if (Path.GetExtension (path) != "")
{
path = path.Replace(Path.GetFileName(AssetDatabase.GetAssetPath(Selection.activeObject)), "");
}

gos = PrefabTools.GetPrefabsAtPath(path);
}

var material_000000 = AssetDatabase.LoadAssetAtPath<Material>("Assets/Fonts/Candarab SDF Outline 000000.mat");
var material_002765 = AssetDatabase.LoadAssetAtPath<Material>("Assets/Fonts/Candarab SDF Outline 00 27 65.mat");
var material_61615 = AssetDatabase.LoadAssetAtPath<Material>("Assets/Fonts/Candarab SDF Outline 61 6 15.mat");
var material_117520 = AssetDatabase.LoadAssetAtPath<Material>("Assets/Fonts/Candarab SDF Outline 117 52 0.mat");
var material_123785 = AssetDatabase.LoadAssetAtPath<Material>("Assets/Fonts/Candarab SDF Outline 123 78 5.mat");
var material_19314293 = AssetDatabase.LoadAssetAtPath<Material>("Assets/Fonts/Candarab SDF Outline 193 142 93.mat");
var material_19716266 = AssetDatabase.LoadAssetAtPath<Material>("Assets/Fonts/Candarab SDF Outline 197 162 66.mat");
var material_2562060 = AssetDatabase.LoadAssetAtPath<Material>("Assets/Fonts/Candarab SDF Outline 256 206 0.mat");

Debug.Log(material_000000);
Debug.Log(material_002765);
Debug.Log(material_61615);
Debug.Log(material_117520);
Debug.Log(material_123785);
Debug.Log(material_19314293);
Debug.Log(material_19716266);
Debug.Log(material_2562060);

foreach (var go in gos)
{
var outlines = go.GetComponentsInChildren<Outline>(true);
foreach (var o in outlines)
{
var r = (int) (o.effectColor.r * 256);
var g = (int) (o.effectColor.g * 256);
var b = (int) (o.effectColor.b * 256);

Material m = null;

if (r == 0 && g == 27 && b == 65)
{
m = material_002765;
}
else if (r == 61 && g == 6 && b == 15)
{
m = material_61615;
}
else if (r == 117 && g == 52 && b == 0)
{
m = material_117520;
}
else if (r == 128 && g == 78 && b == 5)
{
m = material_123785;
}
else if (r == 193 && g == 142 && b == 93)
{
m = material_19314293;
}
else if (r == 197 && g == 162 && b == 66)
{
m = material_19716266;
}
else if (r == 256 && g == 206 && b == 0)
{
m = material_2562060;
}
else
{
m = material_000000;
}

var tmpro = o.GetComponent<TextMeshProUGUI>();
if (tmpro != null)
{
tmpro.fontSharedMaterial = m;
Object.DestroyImmediate(o, true);
EditorUtility.SetDirty(go);
}
else
{
Debug.LogError("outline without tm pro!?!?!?!? " + o.gameObject);
}
}
}
}

7) Once that's all done you can replace all instances of TextProxy in your code with TextMeshProUGUI, revert the changes to the TextMeshProUGUI class, and set the DLLs back to the original ones.

It worked really well for us, and as the process itself is not that time consuming (our game is not huge though I suppose) we were able to easily iterate. Of course it doesn't take into account special cases like curve text but in our case there were few enough of those to deal with them separately.

Hope it helps!

G.

malch

  • Newbie
  • *
  • Posts: 10
Re: Converting existing UnityEngine.UI.Text objects into TextMeshProUGUI
« Reply #1 on: January 17, 2017, 05:06:19 AM »
Thanks for the tool, it was very helpful. Here is an updated version (TextMesh Pro 1.0.55.52 b3, Unity 5.5.0p3).


* Make sure to use the correct Unity-Technologies-ui matching your Unity version.
* In the VS solution add the TextProxy.cs as described and also add a link to the project "UnityEngine.UI-Editor"
* The Unity GUISystem on Windows is located at: C:\Program Files\YOUR_UNITY_INSTALLATION\Editor\Data\UnityExtensions\Unity\GUISystem
* After updating the DLLs also delete YOUR_PROJECT\Library\UnityAssemblies so the new .dlls get loaded
* The conversion code snippets (all the static methods) go to a file called PrefabTools.cs in Assets\Editor
* The class "TextMeshProFont" in TextWrapper.cs is now called "TMP_FontAsset"
* Note the Input fields are not supported with this tool

Text.cs
Code: [Select]
    public class Text : TextProxy, ILayoutElement
{
        ...
        public override string text

TextProxy.cs
Code: [Select]
    public class TMP_Text : TextProxy
    {
...
        public override string text

TextWrapper.cs
Code: [Select]
using TMPro;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;

public class TextWrapper
{
...
static TMP_FontAsset font = null;
static TMP_FontAsset Font
{
get
{
if (font == null)
{
font = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>("Assets/Fonts/Quantico-Bold.asset");
}

return font;
}
}


PrefabTools.cs
Code: [Select]
using System.Collections.Generic;
using System.IO;
using System.Linq;
using TMPro;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;

namespace Assets.Editor
{
    public static class PrefabTools
    {
    ...