TextMeshPro(Unity)实现组合后的Emoji

前言

        本文整体思想就是遍历字符串找出组合后的Emoji信息并将其重新赋值于TextMeshPro。这样做固然可以满足复合Emoji显示的需求,但是这样同样会照成额外的开销。如果要更好的性能,那么只能自己写一份图文混排的组件或是以某种方法修改TextMeshPro的文本处理使其可以直接支持。

        如果你不知道如何在TextMeshPro中显示Emoji(非组合),你可以去看一下这篇文章:Unity中使用TextMeshPro打出Emoji表情。不过这篇文章中的TextMeshPro是预实验的版本。如果你不行使用预实验的版本,那么你可以看完上面的文章后再看一下这篇文章:Unity使用TextMeshPro加载Emoji

环境

Windows10
com.unity.textmeshpro 3.0.9
Unity 2022.3.8f1c1

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
using System.Text;
using TMPro;
using UnityEngine;

namespace Test
{
[ExecuteAlways]
public class TMPTest : TextMeshPro
{
private string originText = string.Empty;
public override string text
{
get => originText;
set
{
originText = value;
StringBuilder strb = new StringBuilder();
var unicodes = originText.ToCharArray();
for(int i = 0;i < unicodes.Length; ++i)
{
var curChar = unicodes[i];
var curVal = (int)curChar;
if ((i + 1) < unicodes.Length && IsEmojiHead(curVal))
{
i++;
var realVal = ToEmojiVal(curVal, unicodes[i]);
if ((i + 1) < unicodes.Length)
{
curChar = unicodes[i + 1];
if ((i + 2) < unicodes.Length && IsEmojiHead(curVal))
{
i+=2;
var tmpStrb = new StringBuilder();
// 获得上一个的Unicode值
tmpStrb.AppendFormat("{0:x4}", realVal);
realVal = ToEmojiVal(curVal, unicodes[i]);
tmpStrb.Append('-');
// 使用‘-’将这两个Unicode值进行拼接
tmpStrb.AppendFormat("{0:x4}", realVal);
var unicodeVal = (uint)TMP_TextUtilities.StringHexToInt(tmpStrb.ToString());
bool isUsingAlternativeTypeface;
TMP_TextElement character = GetTextElement(unicodeVal, m_currentFontAsset, m_FontStyleInternal, m_FontWeightInternal, out isUsingAlternativeTypeface);
// 表示资源中存在这样的复合值
if (character != null)
{
strb.AppendFormat("<sprite={0}>", character.glyphIndex);
}
else
{
strb.Append(unicodes[i - 3]);
strb.Append(unicodes[i - 2]);
strb.Append(curChar);
strb.Append(unicodes[i]);
}
}
}
else
{
strb.Append(curChar);
strb.Append(unicodes[i]);
}
}
else strb.Append(curChar);
}
base.text = strb.ToString();
}
}

private bool IsEmojiHead(int val)
{
return val >= 0xD800 && val <= 0xDBFF;
}

private int ToEmojiVal(int head,int end)
{
return 0x10000 + (head - 0xD800) * 0x400 + (end - 0xDC00);
}

TMP_TextElement GetTextElement(uint unicode, TMP_FontAsset fontAsset, FontStyles fontStyle, FontWeight fontWeight, out bool isUsingAlternativeTypeface)
{
TMP_Character character = TMP_FontAssetUtilities.GetCharacterFromFontAsset(unicode, fontAsset, false, fontStyle, fontWeight, out isUsingAlternativeTypeface);

if (character != null)
return character;

// Search potential list of fallback font assets assigned to the font asset.
if (fontAsset.fallbackFontAssetTable != null && fontAsset.fallbackFontAssetTable.Count > 0)
character = TMP_FontAssetUtilities.GetCharacterFromFontAssets(unicode, fontAsset, fontAsset.fallbackFontAssetTable, true, fontStyle, fontWeight, out isUsingAlternativeTypeface);

if (character != null)
{
// Add character to font asset lookup cache
//fontAsset.AddCharacterToLookupCache(unicode, character);

return character;
}

// Search for the character in the primary font asset if not the current font asset
if (fontAsset.instanceID != m_fontAsset.instanceID)
{
// Search primary font asset
character = TMP_FontAssetUtilities.GetCharacterFromFontAsset(unicode, m_fontAsset, false, fontStyle, fontWeight, out isUsingAlternativeTypeface);

// Use material and index of primary font asset.
if (character != null)
{
m_currentMaterialIndex = 0;
m_currentMaterial = m_materialReferences[0].material;

// Add character to font asset lookup cache
//fontAsset.AddCharacterToLookupCache(unicode, character);

return character;
}

// Search list of potential fallback font assets assigned to the primary font asset.
if (m_fontAsset.fallbackFontAssetTable != null && m_fontAsset.fallbackFontAssetTable.Count > 0)
character = TMP_FontAssetUtilities.GetCharacterFromFontAssets(unicode, fontAsset, m_fontAsset.fallbackFontAssetTable, true, fontStyle, fontWeight, out isUsingAlternativeTypeface);

if (character != null)
{
// Add character to font asset lookup cache
//fontAsset.AddCharacterToLookupCache(unicode, character);

return character;
}
}

// Search for the character in potential local Sprite Asset assigned to the text object.
if (m_spriteAsset != null)
{
TMP_SpriteCharacter spriteCharacter = TMP_FontAssetUtilities.GetSpriteCharacterFromSpriteAsset(unicode, m_spriteAsset, true);

if (spriteCharacter != null)
return spriteCharacter;
}

// Search for the character in the list of fallback assigned in the TMP Settings (General Fallbacks).
if (TMP_Settings.fallbackFontAssets != null && TMP_Settings.fallbackFontAssets.Count > 0)
character = TMP_FontAssetUtilities.GetCharacterFromFontAssets(unicode, fontAsset, TMP_Settings.fallbackFontAssets, true, fontStyle, fontWeight, out isUsingAlternativeTypeface);

if (character != null)
{
// Add character to font asset lookup cache
//fontAsset.AddCharacterToLookupCache(unicode, character);

return character;
}

// Search for the character in the Default Font Asset assigned in the TMP Settings file.
if (TMP_Settings.defaultFontAsset != null)
character = TMP_FontAssetUtilities.GetCharacterFromFontAsset(unicode, TMP_Settings.defaultFontAsset, true, fontStyle, fontWeight, out isUsingAlternativeTypeface);

if (character != null)
{
// Add character to font asset lookup cache
//fontAsset.AddCharacterToLookupCache(unicode, character);

return character;
}

// Search for the character in the Default Sprite Asset assigned in the TMP Settings file.
if (TMP_Settings.defaultSpriteAsset != null)
{
TMP_SpriteCharacter spriteCharacter = TMP_FontAssetUtilities.GetSpriteCharacterFromSpriteAsset(unicode, TMP_Settings.defaultSpriteAsset, true);

if (spriteCharacter != null)
return spriteCharacter;
}

if (character == null)
{

// Check for the missing glyph character in the currently assigned font asset and its fallbacks
character = TMP_FontAssetUtilities.GetCharacterFromFontAsset(unicode, m_currentFontAsset, true, m_FontStyleInternal, m_FontWeightInternal, out isUsingAlternativeTypeface);

if (character == null)
{
// Search for the missing glyph character in the TMP Settings Fallback list.
if (TMP_Settings.fallbackFontAssets != null && TMP_Settings.fallbackFontAssets.Count > 0)
character = TMP_FontAssetUtilities.GetCharacterFromFontAssets(unicode, m_currentFontAsset, TMP_Settings.fallbackFontAssets, true, m_FontStyleInternal, m_FontWeightInternal, out isUsingAlternativeTypeface);
}

if (character == null)
{
// Search for the missing glyph in the TMP Settings Default Font Asset.
if (TMP_Settings.defaultFontAsset != null)
character = TMP_FontAssetUtilities.GetCharacterFromFontAsset(unicode, TMP_Settings.defaultFontAsset, true, m_FontStyleInternal, m_FontWeightInternal, out isUsingAlternativeTypeface);
}

if (character == null)
{
// Use Space (32) Glyph from the currently assigned font asset.
unicode = 32;
character = TMP_FontAssetUtilities.GetCharacterFromFontAsset(unicode, m_currentFontAsset, true, m_FontStyleInternal, m_FontWeightInternal, out isUsingAlternativeTypeface);
}

if (character == null)
{
// Use End of Text (0x03) Glyph from the currently assigned font asset.
unicode = 0x03;
character = TMP_FontAssetUtilities.GetCharacterFromFontAsset(unicode, m_currentFontAsset, true, m_FontStyleInternal, m_FontWeightInternal, out isUsingAlternativeTypeface);
}
}

return character;
}

// 仅在编辑器下执行
#if UNITY_EDITOR
protected override void OnValidate()
{
if (base.text != originText)
text = base.text;
}
#endif
}
}

代码核心逻辑

        正如前言所说,代码的核心逻辑就是遍历字符串并找出组合后的Emoji信息。而关于如何识别Emoji,大家可以去看一下这篇文章——Emoji 识别与过滤。在我们找到这些复合的Emoji字符信息后,我们将这些字符修改为<sprite=x>(这个是TextMeshPro显示图片信息的一种技巧,我也是在网上冲浪的时候看到视频介绍的。原视频——TMP迟早能用上的技巧)。其中x表示你存储复合Emoji表情信息的TMP_Sprite Asset对应的下标信息。

        在源码中你可能会有疑惑,为什么我要将两个Emoji表情的unicode值使用‘-’拼接后再使用TMP_TextUtilities.StringHexToInt函数转一个uint值。这是因为我这里查找这个下标的方法是复制了TextMeshPro自身查找的实现。而查找中我必须传一个uint值。这个uint值是生成TMP_Sprite Asset对应的名字信息。而我复合Emoji的名字就是将两个Emoji表情的unicode值使用‘-’拼接。因为我不想多写一些其他的逻辑所以才使用了这样的方法。大家在工程中使用的话,最好将这段查找的代码修改一下。比如自己做一份复合Emoji信息的映射,然后去查找。

缺点

        在实际使用的过程中,我还是发现这样做其实有挺多缺点的。

        因为我不能修改TextMeshPro原本的字符处理逻辑。所以对于TextMeshPro中的组件比如TextMeshProTextMeshProUGUI等组件,我们都需要进行一次重写。虽然我朋友提出可以监听这些组件的修改然后进行额外操作,但是这样的操作仍然算是麻烦。你仍然要在使用TextMeshPro中组件的物体上挂着额外的脚本。且性能方面也多了额外监听的消耗。

        TextMeshPro其实也提供了一个输入框的组件。因为TMP_InputField中的text变量不能进行重载,所以我只能监听它的值改变并按照上述方式修改。这时候Emoji显示是正确的,但是当你复制输入框的信息时它就变成了<sprite=x>而不是之前的Emoji文本。我现在也没办法解决这难受的事情。

参考内容

  1. Unity中使用TextMeshPro打出Emoji表情:https://blog.csdn.net/hmf532123602/article/details/127124752
  2. TMP迟早能用上的技巧:https://www.bilibili.com/video/BV1a1421B7ss/?spm_id_from=333.880.my_history.page.click
  3. Emoji 识别与过滤:https://www.jianshu.com/p/42fd6f84c27a