天天看点

Unity表情聊天(NGUI图文混排)

图文混排

从字面意思来理解:就是图片和文字混合在一起。不知道这样的的定义是否正确,起码我是这样理解的,在游戏开发过程中,如果单单从业务逻辑去看的话,图文混排算是比较复杂的。个人感觉也是必须会的技能。

原理

我们期待在文本的合适地方插入我们需要显示的表情。所以我们要取插入表情的位置,但是一条聊天的信息是一段文本的字符串,是一个整体,每一个字符都不是单个对象,所以要在文本字符串中取合适的位置就不能用平时的方法(ps:UILabe的本质是一张面片),好在NGUI底层给我们提供了顶点信息的集合。

/// <summary>
	/// Widget's vertices (before they get transformed).
	/// </summary>

	public BetterList<Vector3> verts = new BetterList<Vector3>();
           

现在我们已经找到突破口,verts保存的是位置信息,那么只有我们算出表情在第几个字后面就可以取得位置信息了。具体步骤如下:

  • 1 定义表情的数据格式,
    Unity表情聊天(NGUI图文混排)
  • 2 在写入字符串时候,加上特效的标记,然后用正则表达去匹配和替换字符。
  • 3 遍历整个文本,把之前标记的位置记录下来。并且使用空格去替换标记的内容。
  • 4 创建表情对象,把第三步取到的位置信息赋值给刚刚创建的表情对象

核心代码

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using UnityEngine;

public class UIEmojiWrapper
{
    private bool hasAtlas = false;
    public UIAtlas mAtlas;
    private GameObject emojiPrefab;
    private Dictionary<string, string> emojiName = new Dictionary<string, string>();
    private List<Expression> listCfgData = new List<Expression>();
    private Queue<GameObject> freeSprite = new Queue<GameObject>();

    private List<GameObject> usedSprite = new List<GameObject>();

    private Vector3 OutOffScreen = new Vector3(10000f, 10000f, 10000f);

    private static UIEmojiWrapper sInstance;
    public static UIEmojiWrapper Instance
    {
        get
        {
            if (sInstance == null)
            {
                sInstance = new UIEmojiWrapper();
            }

            return sInstance;
        }
    }

    public void Init(UIAtlas atlas)
    {
        if (!hasAtlas)
        {
            if (atlas == null)
            {
                Debug.LogError("[UIEmojiWrapper Atlas is null]");
                return;
            }
            mAtlas = atlas;
            //预分配
            AddEmojiName();
            emojiPrefab = new GameObject();
            emojiPrefab.layer = 8;
            hasAtlas = true;
        }

    }

    public GameObject EmojiPrefab
    {
        get
        {
            if (emojiPrefab == null)
            {
                emojiPrefab = new GameObject();
                emojiPrefab.layer = 8;
            }
            return emojiPrefab;
        }
    }

    //表情字典
    public void AddEmojiName()
    {
        List<Expression> listCfgEmo = SetlistExpression();
        if (listCfgEmo == null)
        {
            return;
        }
        listCfgData.Clear();
        listCfgData = listCfgEmo;
        for (int i = 0; i < listCfgEmo.Count; i++)
        {
            int m_key = (int)listCfgEmo[i].key;
            string name = listCfgEmo[i].value;
            string sprKey = GetConvertedInt32(m_key.ToString());
            emojiName.Add(sprKey, name);
        }
    }

    public string GetConvertedString(string inputString)
    {
        string[] converted = inputString.Split('-');
        for (int j = 0; j < converted.Length; j++)
        {
            int value = Convert.ToInt32(converted[j], 16);
            converted[j] = char.ConvertFromUtf32(value);
        }
        return string.Join(string.Empty, converted);
    }

    public string GetConvertedInt32(string inputString)
    {
        int value = int.Parse(inputString);
        string[] converted = new string[1];
        converted[0] = char.ConvertFromUtf32(value);
        return string.Join(string.Empty, converted);
    }

    //从对象池取对象
    public string GetEmoji(string encode)
    {
        string em;
        if (emojiName.TryGetValue(encode, out em))
        {
            return em;
        }
        return null;
    }

    public bool HasEmoji(string key)
    {
        // 120000 120001 120002
        return emojiName.ContainsKey(key);
    }

    public List<Expression> GetListCfgData()
    {
        return listCfgData;
    }

    // 取表情数据Key
    public int GetEmojiKey(string _value)
    {
        if (listCfgData.Count > 0)
        {
            for (int i = 0; i < listCfgData.Count; i++)
            {
                if (_value == listCfgData[i].name)
                {
                    return (int)listCfgData[i].key;
                }
            }
        }
        return -1;
    }

    public void OnPostFill(UIWidget widget, int bufferOffset, BetterList<Vector3> verts, BetterList<Vector2> uvs, BetterList<Color32> cols)
    {
        if (widget != null)
        {
            if (!widget.isVisible)
            {
                UISprite spt = widget as UISprite;
                if (spt != null)
                {
                    PushEmoji(spt.gameObject);
                }
            }
        }
    }

    public GameObject PopEmoji()
    {
        if (freeSprite.Count <= 0)
        {
            if (emojiPrefab == null)
                return null;

            GameObject tran = GameObject.Instantiate(emojiPrefab) as GameObject;
            freeSprite.Enqueue(tran);
        }

        GameObject sptRet = freeSprite.Dequeue();

        if (sptRet != null)
        {
            usedSprite.Add(sptRet);
        }
        return sptRet;
    }


    public void PushEmoji(GameObject spt)
    {
        //spt.transform.localPosition = OutOffScreen;
        // spt.transform.parent = null;
        freeSprite.Enqueue(spt);
    }

    public void PushEmoji(ref List<GameObject> list)
    {
        for (int i = 0, cnt = list.Count; i < cnt; i++)
        {
            PushEmoji(list[i]);
        }

        list.Clear();
    }

    /// <summary>
    /// 字符串替换方法
    /// </summary>
    /// <param name="myStr">需要替换的字符串</param>
    /// <param name="displaceA">需要替换的字符</param>
    /// <param name="displaceB">将替换为</param>
    /// <returns></returns>
    public string Displace(string myStr, string displaceA, string displaceB)
    {
        string[] strArrayA = Regex.Split(myStr, displaceA);
        for (int i = 0; i < strArrayA.Length - 1; i++)
        {
            strArrayA[i] += displaceB;
        }
        string returnStr = "";
        foreach (string var in strArrayA)
        {
            returnStr += var;
        }
        return returnStr;
    }

    //建议用json或者其他的数据格式
    public static List<Expression> SetlistExpression()
    {
        List<Expression> data = new List<Expression>();
        data.Add(new Expression(120000, "ziya", 1, "龇牙"));
        data.Add(new Expression(120001, "zhouma", 1, "咒骂"));
        data.Add(new Expression(120002, "youxian", 1, "悠闲"));
        data.Add(new Expression(120003, "weixiao", 1, "微笑"));
        data.Add(new Expression(120004, "qiaoda", 1, "敲打"));
        data.Add(new Expression(120005, "nanguo", 1, "难过"));
        data.Add(new Expression(120006, "daxiao", 1, "大笑"));
        data.Add(new Expression(120008, "deng", 1, "瞪"));
        data.Add(new Expression(120009, "dese", 1, "得瑟"));
        data.Add(new Expression(120010, "dese", 1, "得意"));
        data.Add(new Expression(120011, "lianhong", 1, "脸红"));
        data.Add(new Expression(120012, "keai", 1, "可爱"));
        data.Add(new Expression(120013, "han", 1, "汗"));
        data.Add(new Expression(120014, "ganga", 1, "尴尬"));
        data.Add(new Expression(120015, "se", 1, "色"));
        data.Add(new Expression(120016, "kun", 1, "困"));
        data.Add(new Expression(120017, "ku", 1, "酷"));
        data.Add(new Expression(120018, "baiyan", 1, "白眼"));
        data.Add(new Expression(120019, "baoya", 1, "龅牙"));
        data.Add(new Expression(120020, "beida", 1, "被打"));

        return data;
    }

}

public class Expression
{
    public uint key;                // 16进制编号
    public string value;                // 前缀
    public uint sin;                // 是否序列帧
    public string name;				// 名称

    public Expression(uint key, string value, uint sin, string name)
    {
        this.key = key;
        this.value = value;
        this.sin = sin;
        this.name = name;
    }
}

           
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using UnityEngine;

public class UIEmojiLabel : UILabel
{
    private char emSpace = '\u2001';
    private List<GameObject> mEmojiList = new List<GameObject>();

    public UIAtlas emAtlas = null;

    protected override void Awake()
    {
        base.Awake();
        UIEmojiWrapper.Instance.Init(emAtlas);
    }

    protected override void OnStart()
    {
        base.OnStart();
     
        //表情统一管理器,只初始化一次
    }

    //自动转码
    public override void OnFill(BetterList<Vector3> verts, BetterList<Vector2> uvs, BetterList<Color32> cols)
    {
        base.OnFill(verts, uvs, cols);
       // StartCoroutine(SetUITextThatHasEmoji(text));
    }

    public void ShowEmojiText(string strText)
    {
        //strText = "图形测试 (ziya) 呵呵 (youxian)"
        //找到表情特殊标记()
        Regex reg = new Regex(@"(?is)(?<=\()[^\)]+(?=\))");
        MatchCollection mc = reg.Matches(strText);
        if (mc != null)
        {
            for (int i = 0; i < mc.Count; i++)
            {
                string oldStr = string.Format("({0})", mc[i].Value);   //旧字符
                int key = UIEmojiWrapper.Instance.GetEmojiKey(mc[i].Value);
                if (key > 0)
                {
                    string newStr = UIEmojiWrapper.Instance.GetConvertedInt32(key.ToString()); //薪字符
                    strText = strText.Replace(oldStr, newStr);
                }
            }
        }
        text = strText;
        StartCoroutine(SetUITextThatHasEmoji(strText));
    }

    private struct PosStringTuple
    {
        public int pos;
        public string emoji;

        public PosStringTuple(int p, string s)
        {
            this.pos = p;
            this.emoji = s;
        }
    }

    private void DestroyEmojiSpr()
    {
        if (this.mEmojiList.Count > 0)
        {
            for (int i = 0; i < this.mEmojiList.Count; i++)
            {
                Destroy(this.mEmojiList[i]);
            }
            this.mEmojiList.Clear();
        }
    }

     public IEnumerator SetUITextThatHasEmoji(string inputString)
    {
        inputString = inputString.Replace("  ", "&&");
        inputString = inputString.Replace(" ", "&");

        List<PosStringTuple> emojiReplacements = new List<PosStringTuple>();
        StringBuilder sb = new StringBuilder();

        //先回收
        DestroyEmojiSpr();
        int i = 0;
        while (i < inputString.Length)
        {
            string singleChar = inputString.Substring(i, 1);
            string doubleChar = "";
            string fourChar = "";

            if (i < (inputString.Length - 1))
            {
                doubleChar = inputString.Substring(i, 2);
            }

            if (i < (inputString.Length - 3))
            {
                fourChar = inputString.Substring(i, 4);
            }
            if (UIEmojiWrapper.Instance.HasEmoji(fourChar))
            {
                // Check 64 bit emojis first
                sb.Append(emSpace);
                int blankSpaceConut = GetBlankSpaceConut(sb);
                emojiReplacements.Add(new PosStringTuple(sb.Length - 1, fourChar));
                i += 4;
            }
            else if (UIEmojiWrapper.Instance.HasEmoji(doubleChar))
            {
                sb.Append(emSpace);
                emojiReplacements.Add(new PosStringTuple(sb.Length - 1, doubleChar));
                i += 2;
            }
            else if (UIEmojiWrapper.Instance.HasEmoji(singleChar))
            {
                sb.Append(emSpace);
                emojiReplacements.Add(new PosStringTuple(sb.Length - 1, singleChar));
                i++;
            }
            else
            {
                sb.Append(inputString[i]);
                i++;
            }
        }
        this.text = sb.ToString();
        yield return null;
        for (int j = 0; j < emojiReplacements.Count; j++)
        {
            int emojiIndex = emojiReplacements[j].pos;
            //表情替换,计算位置,大小
            GameObject go = GameObject.Instantiate(UIEmojiWrapper.Instance.EmojiPrefab);
            mEmojiList.Add(go);
            if (go != null)
            {
                UISprite spt = go.AddComponent<UISprite>();
                string emoji = UIEmojiWrapper.Instance.GetEmoji(emojiReplacements[j].emoji);
                if (!string.IsNullOrEmpty(emoji))
                {
                    spt.atlas = UIEmojiWrapper.Instance.mAtlas;
                    spt.name = emoji;
                    spt.spriteName = emoji + "_1";

                    spt.width = this.ChatPrintedSize;
                    spt.height = this.ChatPrintedSize;
                    spt.depth = this.depth + 10;
                    spt.transform.parent = this.transform;
                    spt.transform.localScale = Vector3.one;
                    Vector3 pos = new Vector3();
                    try
                    {
                        pos = new Vector3(this.geometry.verts[emojiIndex * 4].x + spt.width / 2, (this.geometry.verts[emojiIndex * 4].y + spt.height / 2) - 8);
                        spt.transform.localPosition = pos;
                        UISpriteAnimation sprAni = go.AddComponent<UISpriteAnimation>();
                        sprAni.namePrefix = emoji + "_";
                        sprAni.framesPerSecond = 3;
                        sprAni.Snap = false;
                        sprAni.Play();
                        isVertsBuff = false;
                    }
                    catch
                    {
                        Debug.LogError("geometry.verts == null");
                        isVertsBuff = true;
                        ShowEmojiText(oldText);
                    }
                }
            }
        }
        if (!isVertsBuff)
        {
            this.text = text.Replace("&&", "  ");
            this.text = text.Replace("&", " ");
        }
    }
}
           
#if !UNITY_3_5 && !UNITY_FLASH
#define DYNAMIC_FONT
#endif

using UnityEngine;
using UnityEditor;

/// <summary>
/// Inspector class used to edit UILabels.
/// </summary>

[CanEditMultipleObjects]
#if UNITY_3_5
[CustomEditor(typeof(UILabel))]
#else
[CustomEditor(typeof(UILabel), true)]
#endif
public class UIEmojiLabelInspector : UILabelInspector
{


    /// <summary>
    /// Draw the label's properties.
    /// </summary>

    protected override bool ShouldDrawProperties()
    {
        bool isValid = base.ShouldDrawProperties();

        EditorGUI.BeginDisabledGroup(!isValid);
        NGUIEditorTools.DrawProperty("Atlas", serializedObject, "emAtlas");//表情所在的图集Atlas
        EditorGUI.EndDisabledGroup();
        return isValid;
    }
}
           

主要事项

1 上面的代码改了一下部分NGUI的接口,直接拷贝会报错,你可以自己手动加,都是很简单的接口。我也会上传一份dome

2 在Lua层调C#的时候,有机会出现第一次会报verts为空,我还没有找到原因,欢迎大神指点,我现在的解决方法是延迟执行。

如果大佬们有好的方法,也求推荐。

3 看看 最终效果是带动态的哦!!

Unity表情聊天(NGUI图文混排)

表情聊天— 图文混排下载