Unity图文混排EmojiText的使用方式和注意事项

​​​​​​​ 效果演示:

使用方式:

1、导入表情

2、设置图片格式

3、生成表情图集

4、创建/修改目标材质球

5、测试

修复换行问题

修复前:

修复后:

修复代码:

组件扩展

1、右键扩展

2、组件归类:

注意事项

文章引用:


EmojiText组件代码来源工程地址:https://github.com/zouchunyi/EmojiText

效果演示:

使用方式:

1、导入表情

将表情图片素材(png格式)导入到Unity工程中的这个目录中:Assets/Emoji/Input,目录可以按需更换。

注意表情图片的尺寸必须一致,命名规范:纯字母.png或 纯字母_数字.png,例:a.png, b_0.png,b_1.png。

同一个表情的序列帧图片,以_数字结尾。

2、设置图片格式

设置图片格式为Default,设置Non-Power of 2(2的n次方)为ToNearest,勾选Read/Write Enabled。最后点击Apply按钮。

3、生成表情图集

点击菜单EmojiText/Build Emoji后,会按照EmojiBuilder脚本中的默认值进行创建图集保存数据,为了方便操作在这里扩展成一个UnityEditor窗口。

cs 复制代码
/*
    Description:Create the Atlas of emojis and its data texture.
    How to use?
    1)
        Put all emojies in Asset/Framework/Resource/Emoji/Input.
        Multi-frame emoji name format : Name_Index.png , Single frame emoji format: Name.png
    2)
        Excute EmojiText->Build Emoji from menu in Unity.
    3)
        It will outputs two textures and a txt in Emoji/Output.
        Drag emoji_tex to "Emoji Texture" and emoji_data to "Emoji Data" in UGUIEmoji material.
    4)
        Repair the value of "Emoji count of every line" base on emoji_tex.png.
    5)
        It will auto copys emoji.txt to Resources, and you can overwrite relevant functions base on your project.
    
    Author:zouchunyi
    E-mail:zouchunyi@kingsoft.com
*/
using System;
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
using System.IO;
public class EmojiBuilder : EditorWindow  {
    private static string OutputPath = "Assets/Emoji/Output/";
    private static string InputPath = "Assets/Emoji/Input/";
    private const string CopyTargetPath = "Assets/Resources/emoji.txt";
    private static readonly Vector2[] AtlasSize = new Vector2[]{
        new Vector2(32,32),
        new Vector2(64,64),
        new Vector2(128,128),
        new Vector2(256,256),
        new Vector2(512,512),
        new Vector2(1024,1024),
        new Vector2(2048,2048)
    };
    struct EmojiInfo
    {
        public string key;
        public string x;
        public string y;
        public string size;
    }
    private static int EmojiSize = 32;//the size of emoji.
    [MenuItem("EmojiText/Build Emoji Wnd")]
    public static void BuildEmojiWnd()
    {
        GetWindow<EmojiBuilder>();
    }
    private void OnGUI()
    {
        
        InputPath = EditorGUILayout.TextField("表情散图存放路径", InputPath);
        OutputPath = EditorGUILayout.TextField("表情图集生成路径", OutputPath);
        EditorGUILayout.HelpBox("注意:每个表情图片尺寸需要统一。",MessageType.Warning);
        EmojiSize = EditorGUILayout.IntField("单个表情图尺寸", EmojiSize);
        if (GUILayout.Button("生成表情图集")) {
            BuildEmoji (); 
        }
    }
    // [MenuItem("EmojiText/Build Emoji")]
    public static void BuildEmoji()
    {
        // List<char> keylist = new List<char> ();
        // for(int i = 0; i<100; i++)
        // {
        //  keylist.Add(i.ToString());
        // }
        // for (int i = 48; i <= 57; i++) {
        //  keylist.Add (System.Convert.ToChar(i));//0-9
        // }
        // for (int i = 65; i <= 90; i++) {
        //  keylist.Add (System.Convert.ToChar(i));//A-Z
        // }
        // for (int i = 97; i <= 122; i++) {
        //  keylist.Add (System.Convert.ToChar(i));//a-z
        // }
        //search all emojis and compute they frames.
        Dictionary<string,int> sourceDic = new Dictionary<string,int> ();
        string[] files = Directory.GetFiles (Application.dataPath.Replace("Assets", "") + InputPath,"*.png");
        for (int i = 0; i < files.Length; i++) {
            string[] strs = files [i].Split ('/');
            string[] strs2 = strs [strs.Length - 1].Split ('.');
            string filename = strs2 [0];
            string[] t = filename.Split('_');
            string id = t [0];
            if (sourceDic.ContainsKey(id)) {
                sourceDic[id]++;
            } else {
                sourceDic.Add (id, 1);
            }
        }
            
        //create the directory if it is not exist.
        if (!Directory.Exists (OutputPath)) {
            Directory.CreateDirectory (OutputPath);
        }   
        Dictionary<string,EmojiInfo> emojiDic = new Dictionary<string, EmojiInfo> ();
        int totalFrames = 0;
        foreach (int value in sourceDic.Values) {
            totalFrames += value;
        }
        Vector2 texSize = ComputeAtlasSize (totalFrames);
        Texture2D newTex = new Texture2D ((int)texSize.x, (int)texSize.y, TextureFormat.ARGB32, false);
        Texture2D dataTex = new Texture2D ((int)texSize.x / EmojiSize, (int)texSize.y / EmojiSize, TextureFormat.ARGB32, false);
        int x = 0;
        int y = 0;
        int keyindex = 0;
        foreach (string key in sourceDic.Keys) {
            for (int index = 0; index < sourceDic[key]; index++) {
                
                string path = InputPath + key;
                if (sourceDic[key] == 1) {
                    path += ".png";
                } else {
                    path += "_" + (index + 1).ToString() + ".png";
                }
                Texture2D asset = AssetDatabase.LoadAssetAtPath<Texture2D> (path);
                Color[] colors = asset.GetPixels (0); 
                for (int i = 0; i < EmojiSize; i++) {
                    for (int j = 0; j < EmojiSize; j++) {
                        newTex.SetPixel (x + i, y + j, colors [i + j * EmojiSize]);
                    }
                }
                string t = System.Convert.ToString (sourceDic [key] - 1, 2);
                float r = 0, g = 0, b = 0;
                if (t.Length >= 3) {
                    r = t [2] == '1' ? 0.5f : 0;
                    g = t [1] == '1' ? 0.5f : 0;
                    b = t [0] == '1' ? 0.5f : 0;
                } else if (t.Length >= 2) {
                    r = t [1] == '1' ? 0.5f : 0;
                    g = t [0] == '1' ? 0.5f : 0;
                } else {
                    r = t [0] == '1' ? 0.5f : 0;
                }
                dataTex.SetPixel (x / EmojiSize, y / EmojiSize, new Color (r, g, b, 1));
                if (! emojiDic.ContainsKey (key)) {
                    EmojiInfo info;
                    // if (keyindex < keylist.Count)
                    // {
                    //  info.key = "[" + char.ToString(keylist[keyindex]) + "]";
                    // }else
                    // {
                    //  info.key = "[" + char.ToString(keylist[keyindex / keylist.Count]) + char.ToString(keylist[keyindex % keylist.Count]) + "]";
                    // }
                    info.key = "[" + keyindex + "]";
                    info.x = (x * 1.0f / texSize.x).ToString();
                    info.y = (y * 1.0f / texSize.y).ToString();
                    info.size = (EmojiSize * 1.0f / texSize.x).ToString ();
                    emojiDic.Add (key, info);
                    keyindex ++;
                }
                x += EmojiSize;
                if (x >= texSize.x) {
                    x = 0;
                    y += EmojiSize;
                }
            }
        }
        byte[] bytes1 = newTex.EncodeToPNG ();
        string outputfile1 = OutputPath + "emoji_tex.png";
        File.WriteAllBytes (outputfile1, bytes1);
        byte[] bytes2 = dataTex.EncodeToPNG ();
        string outputfile2 = OutputPath + "emoji_data.png";
        File.WriteAllBytes (outputfile2, bytes2);
        using (StreamWriter sw = new StreamWriter (OutputPath + "emoji.txt",false)) {
            sw.WriteLine ("Name\tKey\tFrames\tX\tY\tSize");
            foreach (string key in emojiDic.Keys) {
                sw.WriteLine ("{" + key + "}\t" + emojiDic[key].key + "\t" + sourceDic[key] + "\t" + emojiDic[key].x + "\t" + emojiDic[key].y + "\t" + emojiDic[key].size);
            }
            sw.Close ();
        }
        File.Copy (OutputPath + "emoji.txt",CopyTargetPath,true);
        AssetDatabase.Refresh ();
        FormatTexture ();
        EditorUtility.DisplayDialog ("生成成功", "生成表情图集成功!", "确定");
    }
    private static Vector2 ComputeAtlasSize(int count)
    {
        long total = count * EmojiSize * EmojiSize;
        for (int i = 0; i < AtlasSize.Length; i++) {
            if (total <= AtlasSize [i].x * AtlasSize [i].y) {
                return AtlasSize [i];
            }
        }
        return Vector2.zero;
    }
    private static void FormatTexture() {
        TextureImporter emojiTex = AssetImporter.GetAtPath (OutputPath + "emoji_tex.png") as TextureImporter;
        emojiTex.filterMode = FilterMode.Point;
        emojiTex.mipmapEnabled = false;
        emojiTex.sRGBTexture = true;
        emojiTex.alphaSource = TextureImporterAlphaSource.FromInput;
        emojiTex.textureCompression = TextureImporterCompression.Uncompressed;
        emojiTex.SaveAndReimport ();
        TextureImporter emojiData = AssetImporter.GetAtPath (OutputPath + "emoji_data.png") as TextureImporter;
        emojiData.filterMode = FilterMode.Point;
        emojiData.mipmapEnabled = false;
        emojiData.sRGBTexture = false;
        emojiData.alphaSource = TextureImporterAlphaSource.None;
        emojiData.textureCompression = TextureImporterCompression.Uncompressed;
        emojiData.SaveAndReimport ();
    }
}

生成成功后可以在"表情图集生成路径"中看到有三个文件。

其中emoji文本文件记录了,当前生成的图集中每个表情的数据信息。

该文件会在生成的时候拷贝到Resources目录,该地址可以通过脚本中CopyTargetPath属性值进行指定。

4、创建/修改目标材质球

原工程默认会自带一个材质球"UGUIEmoji",目标位于材质球"Material"文件夹中,如果灭有可以手动创建。右键Shader文件夹中的"UI-EmojiFont"文件可以直接创建目标材质球。也可以创建出来材质球后手动指定材质球的Shader。

将生成好的emoji_data和emoji_tex分别拖放到材质球对应的属性中。

因为生成的图集"emoji_tex"的每一行是4个表情,所以设置Emoji count of every line为4,FrameSpeed是每秒播放序列帧数量,可根据实际情况调整。

5、测试

创建一个空对象,挂载"EmojiText"脚本组件,在输入文本内容"[0]你好[1]",给组件添加改好的材质球,即可看到效果。

修复换行问题

修复前:
修复后:

问题修复需要改动"EmojiText"脚本。修复工程源码来源:https://github.com/ry02/EmojiText

修复代码:
cs 复制代码
// Textは自動改行が入ると、改行コードの位置にもvertsの中に頂点情報が追加されるが、
// 自動改行が入らないと、改行コードのための頂点情報は無いので、Indexを調整する
if (emojiDic.Count > 0)
{
    MatchCollection newLines = Regex.Matches(emojiText, "\\n");
    // TextのRect範囲外は行(lineCount)にならないので、全文字が表示されている(characterCount)かも確認する。
    if (cachedTextGenerator.lineCount == newLines.Count + 1 && emojiText.Length < cachedTextGenerator.characterCount)
    {
        // 絵文字があり、自動改行が入っていないので、indexを改行コードの数だけ調整する
        Dictionary<int, EmojiInfo> emojiDicReplace = new Dictionary<int, EmojiInfo>();
        foreach (var ed in emojiDic)
        {
            int index = ed.Key;
            int offset = 0;
            foreach (Match nl in newLines)
            {
                if (nl.Index < index)
                {
                    offset -= 1;
                }
            }
            emojiDicReplace.Add(index + offset, ed.Value);
        }
        emojiDic = emojiDicReplace;
    }
}

修复后的EmojiText源代码:

cs 复制代码
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
using System.Text.RegularExpressions;
public class EmojiText : Text
{
    private const float ICON_SCALE_OF_DOUBLE_SYMBOLE = 0.7f;
    public override float preferredWidth =>
        cachedTextGeneratorForLayout.GetPreferredWidth(emojiText, GetGenerationSettings(rectTransform.rect.size)) /
        pixelsPerUnit;
    public override float preferredHeight =>
        cachedTextGeneratorForLayout.GetPreferredHeight(emojiText, GetGenerationSettings(rectTransform.rect.size)) /
        pixelsPerUnit;
    private string emojiText => Regex.Replace(text, "\\[[a-z0-9A-Z]+\\]", "%%");
    private static Dictionary<string, EmojiInfo> m_EmojiIndexDict = null;
    struct EmojiInfo
    {
        public float x;
        public float y;
        public float size;
    }
    readonly UIVertex[] m_TempVerts = new UIVertex[4];
    protected override void OnPopulateMesh(VertexHelper toFill)
    {
        if (font == null)
        {
            return;
        }
        if (m_EmojiIndexDict == null)
        {
            m_EmojiIndexDict = new Dictionary<string, EmojiInfo>();
            //load emoji data, and you can overwrite this segment code base on your project.
            TextAsset emojiContent = Resources.Load<TextAsset>("emoji");
            string[] lines = emojiContent.text.Split('\n');
            for (int i = 1; i < lines.Length; i++)
            {
                if (!string.IsNullOrEmpty(lines[i]))
                {
                    string[] strs = lines[i].Split('\t');
                    EmojiInfo info;
                    info.x = float.Parse(strs[3]);
                    info.y = float.Parse(strs[4]);
                    info.size = float.Parse(strs[5]);
                    m_EmojiIndexDict.Add(strs[1], info);
                }
            }
        }
        Dictionary<int, EmojiInfo> emojiDic = new Dictionary<int, EmojiInfo>();
        if (supportRichText)
        {
            int nParcedCount = 0;
            //[1] [123] 替换成#的下标偏移量          
            int nOffset = 0;
            MatchCollection matches = Regex.Matches(text, "\\[[a-z0-9A-Z]+\\]");
            for (int i = 0; i < matches.Count; i++)
            {
                EmojiInfo info;
                if (m_EmojiIndexDict.TryGetValue(matches[i].Value, out info))
                {
                    emojiDic.Add(matches[i].Index - nOffset + nParcedCount, info);
                    nOffset += matches[i].Length - 1;
                    nParcedCount++;
                }
            }
        }
        // We don't care if we the font Texture changes while we are doing our Update.
        // The end result of cachedTextGenerator will be valid for this instance.
        // Otherwise we can get issues like Case 619238.
        m_DisableFontTextureRebuiltCallback = true;
        Vector2 extents = rectTransform.rect.size;
        var settings = GetGenerationSettings(extents);
        cachedTextGenerator.Populate(emojiText, settings);
        Rect inputRect = rectTransform.rect;
        // get the text alignment anchor point for the text in local space
        Vector2 textAnchorPivot = GetTextAnchorPivot(alignment);
        Vector2 refPoint = Vector2.zero;
        refPoint.x = Mathf.Lerp(inputRect.xMin, inputRect.xMax, textAnchorPivot.x);
        refPoint.y = Mathf.Lerp(inputRect.yMin, inputRect.yMax, textAnchorPivot.y);
        // Apply the offset to the vertices
        IList<UIVertex> verts = cachedTextGenerator.verts;
        float unitsPerPixel = 1 / pixelsPerUnit;
        int vertCount = verts.Count;
        // We have no verts to process just return (case 1037923)
        if (vertCount <= 0)
        {
            toFill.Clear();
            return;
        }
        
        // Textは自動改行が入ると、改行コードの位置にもvertsの中に頂点情報が追加されるが、
        // 自動改行が入らないと、改行コードのための頂点情報は無いので、Indexを調整する
        if (emojiDic.Count > 0)
        {
            MatchCollection newLines = Regex.Matches(emojiText, "\\n");
            // TextのRect範囲外は行(lineCount)にならないので、全文字が表示されている(characterCount)かも確認する。
            if (cachedTextGenerator.lineCount == newLines.Count + 1 && emojiText.Length < cachedTextGenerator.characterCount)
            {
                // 絵文字があり、自動改行が入っていないので、indexを改行コードの数だけ調整する
                Dictionary<int, EmojiInfo> emojiDicReplace = new Dictionary<int, EmojiInfo>();
                foreach (var ed in emojiDic)
                {
                    int index = ed.Key;
                    int offset = 0;
                    foreach (Match nl in newLines)
                    {
                        if (nl.Index < index)
                        {
                            offset -= 1;
                        }
                    }
                    emojiDicReplace.Add(index + offset, ed.Value);
                }
                emojiDic = emojiDicReplace;
            }
        }
        
        Vector2 roundingOffset = new Vector2(verts[0].position.x, verts[0].position.y) * unitsPerPixel;
        roundingOffset = PixelAdjustPoint(roundingOffset) - roundingOffset;
        toFill.Clear();
        if (roundingOffset != Vector2.zero)
        {
            for (int i = 0; i < vertCount; ++i)
            {
                int tempVertsIndex = i & 3;
                m_TempVerts[tempVertsIndex] = verts[i];
                m_TempVerts[tempVertsIndex].position *= unitsPerPixel;
                m_TempVerts[tempVertsIndex].position.x += roundingOffset.x;
                m_TempVerts[tempVertsIndex].position.y += roundingOffset.y;
                if (tempVertsIndex == 3)
                {
                    toFill.AddUIVertexQuad(m_TempVerts);
                }
            }
        }
        else
        {
            for (int i = 0; i < vertCount; ++i)
            {
                EmojiInfo info;
                int index = i / 4;
                if (emojiDic.TryGetValue(index, out info))
                {
                    //compute the distance of '[' and get the distance of emoji 
                    //计算2个%%的距离
                    float emojiSize = 2 * (verts[i + 1].position.x - verts[i].position.x) *
                                      ICON_SCALE_OF_DOUBLE_SYMBOLE;
                    float fCharHeight = verts[i + 1].position.y - verts[i + 2].position.y;
                    float fCharWidth = verts[i + 1].position.x - verts[i].position.x;
                    float fHeightOffsetHalf = (emojiSize - fCharHeight) * 0.5f;
                    float fStartOffset = emojiSize * (1 - ICON_SCALE_OF_DOUBLE_SYMBOLE);
                    m_TempVerts[3] = verts[i]; //1
                    m_TempVerts[2] = verts[i + 1]; //2
                    m_TempVerts[1] = verts[i + 2]; //3
                    m_TempVerts[0] = verts[i + 3]; //4
                    m_TempVerts[0].position += new Vector3(fStartOffset, -fHeightOffsetHalf, 0);
                    m_TempVerts[1].position +=
                        new Vector3(fStartOffset - fCharWidth + emojiSize, -fHeightOffsetHalf, 0);
                    m_TempVerts[2].position += new Vector3(fStartOffset - fCharWidth + emojiSize, fHeightOffsetHalf, 0);
                    m_TempVerts[3].position += new Vector3(fStartOffset, fHeightOffsetHalf, 0);
                    m_TempVerts[0].position *= unitsPerPixel;
                    m_TempVerts[1].position *= unitsPerPixel;
                    m_TempVerts[2].position *= unitsPerPixel;
                    m_TempVerts[3].position *= unitsPerPixel;
                    float pixelOffset = emojiDic[index].size / 32 / 2;
                    m_TempVerts[0].uv1 = new Vector2(emojiDic[index].x + pixelOffset, emojiDic[index].y + pixelOffset);
                    m_TempVerts[1].uv1 = new Vector2(emojiDic[index].x - pixelOffset + emojiDic[index].size,
                        emojiDic[index].y + pixelOffset);
                    m_TempVerts[2].uv1 = new Vector2(emojiDic[index].x - pixelOffset + emojiDic[index].size,
                        emojiDic[index].y - pixelOffset + emojiDic[index].size);
                    m_TempVerts[3].uv1 = new Vector2(emojiDic[index].x + pixelOffset,
                        emojiDic[index].y - pixelOffset + emojiDic[index].size);
                    toFill.AddUIVertexQuad(m_TempVerts);
                    i += 4 * 2 - 1;
                }
                else
                {
                    int tempVertsIndex = i & 3;
                    m_TempVerts[tempVertsIndex] = verts[i];
                    m_TempVerts[tempVertsIndex].position *= unitsPerPixel;
                    if (tempVertsIndex == 3)
                    {
                        toFill.AddUIVertexQuad(m_TempVerts);
                    }
                }
            }
        }
        m_DisableFontTextureRebuiltCallback = false;
    }
}

组件扩展

1、右键扩展

在使用中为了方便的创建对象,如同创建Text时的右键菜单,这时候我们可以扩展一下脚本。

新建一个脚本 "EmojiMenu",添加如下代码:

cs 复制代码
private static Transform FindParent()
{
    // 获取当前选择的对象,并检索是否符合条件
    var transform = Selection.activeTransform;
    if (transform == null)
    {
        var canvas = FindObjectOfType<Canvas>();
        if (canvas)
        {
            return canvas.transform;
        }
    }
    else if (transform.GetComponentInParent<Canvas>())
    {
        return transform;
    }
    // 创建一个Canvas对象
    var gameObject = new GameObject("UICanvas");
    if (transform != null)
    {
        gameObject.transform.SetParent(transform);
    }
    gameObject.AddComponent<Canvas>();
    gameObject.AddComponent<CanvasScaler>();
    gameObject.AddComponent<GraphicRaycaster>();
    return gameObject.transform;
}
[MenuItem("GameObject/UI/Emoji Text")]
public static void AddEmojiText(MenuCommand menuCommand)
{
    var child = new GameObject("Emoji Text", typeof(EmojiText));
    RectTransform rectTransform = child.GetComponent<RectTransform>();
    rectTransform.SetParent(FindParent());
    rectTransform.sizeDelta = new Vector2(160, 30);
    rectTransform.localPosition = Vector3.zero;
    rectTransform.localRotation = Quaternion.identity;
    rectTransform.localScale = Vector3.one;
}
2、组件归类:

在"EmojiText"类前面添加即可实现,展开组件菜单的UI项,可以找到当前类型。

cs 复制代码
[AddComponentMenu("UI/EmojiText", 100)]

注意事项

1、存在换行时或者一条字符串中有多个表情时,添加空格会导致文本错乱!!!

2、在使用EmojiText组件时,父节点中如果存在Canvas,请注意Canvas的Additional Shader Channels 属性是否选择了TexCoord1,如果没有选择请勾选该选项,否则会导致图文混排显示异常。

文章引用:

1、GitHub:zouchunyi/EmojiText

2、GitHub:ry02/EmojiText

相关推荐
逐·風6 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
_oP_i8 小时前
Unity Addressables 系统处理 WebGL 打包本地资源的一种高效方式
unity·游戏引擎·webgl
Leoysq17 小时前
【UGUI】实现点击注册按钮跳转游戏场景
游戏·unity·游戏引擎·ugui
_oP_i20 小时前
unity中 骨骼、纹理和材质关系
unity·游戏引擎·材质
Padid1 天前
Unity SRP学习笔记(二)
笔记·学习·unity·游戏引擎·图形渲染·着色器
Tp_jh1 天前
推荐一款非常好用的C/C++在线编译器
linux·c语言·c++·ide·单片机·unity·云原生
dangoxiba2 天前
[Unity Demo]从零开始制作空洞骑士Hollow Knight第十八集补充:制作空洞骑士独有的EventSystem和InputModule
游戏·unity·c#·游戏引擎·playmaker
无敌最俊朗@2 天前
unity3d————屏幕坐标,GUI坐标,世界坐标的基础注意点
开发语言·学习·unity·c#·游戏引擎
_oP_i2 天前
Unity 中使用 WebGL 构建并运行时使用的图片必须使用web服务器上的
前端·unity·webgl
司军礼2 天前
Unity自动打包——Shell交互
unity·游戏引擎·交互