UGF框架本地化在使用时,有三个问题,第一个是策划一般会用表格编辑本地化语言,而框架解析的是Xml文件,缺一个Excel转xml的工具。第二个问题是,编辑器编辑情况下,UI预制件的文本是key值,看不到文字显示后的效果,只有运行时才会动态改变文字,调试起来比较麻烦。第三个是文本的字体不能批量修改。本文就是为了解决这几个问题,做的扩展。
Excel读表插件下载链接,下载后跟框架的ICSharpCode.SharpZipLib插件会冲突,可以删除框架的。
1、Excel转Xml
主要有两个脚本:ExcelConfig.cs,ExcelBuild.cs。语言表的形式如下配置了四种语言,跟框架是一致的。
脚本中路径有所改动,与框架路径不一致,使用时需注意路径。
ExcelConfig.cs脚本如下:
using System.Collections.Generic;
using System.Data;
using System.IO;
using Excel;
using UnityEngine;
public class ExcelConfig
{
/// <summary>
/// 存放excel表文件夹的的路径,本例Excel表放在了"Board/配置表/"当中,Board路径与Assets路径同级
/// </summary>
public static readonly string excelsFolderPath = Application.dataPath + "/../Board/配置表/MK_Language.xlsx";
/// <summary>
/// 存放Excel转化后文件的文件夹路径
/// </summary>
public static string assetPath(string language)
{
return $"Assets/Scripts/Data/Localization/{language}/Dictionaries/Default.xml";
}
}
/// <summary>
/// 处理Excel工具类
/// </summary>
public class ExcelTool
{
/// <summary>
/// 读取表数据,生成对应的数组
/// </summary>
/// <param name="filePath">excel文件全路径</param>
/// <returns>Item数组</returns>
public static ExcelItem[] CreateItemArrayWithExcel(string filePath)
{
//获得表数据
int columnNum = 0, rowNum = 0;
Debug.Log(filePath);
DataRowCollection collect = ReadExcelContext(filePath, ref columnNum, ref rowNum);
Debug.Log("读取到数据表 列数 columnNum : " + columnNum + " ,行数 rowNum: " + rowNum);
// 第一行是标题(头)
//for (int i = 0; i < columnNum; i++)
//{
// rowTitleList.Add(collect[0][i].ToString());
//}
//第二行开始才是数据
List<ExcelItem> array = new List<ExcelItem>();
ExcelItem title = new ExcelItem();
//解析标题数据
title.key = collect[1][0].ToString();
title.Language = new string[4];
title.Language[0] = collect[1][1].ToString();
title.Language[1] = collect[1][2].ToString();
title.Language[2] = collect[1][3].ToString();
title.Language[3] = collect[1][4].ToString();
array.Add(title);
//解析内容数据
for (int i = 4; i < rowNum; i++)
{
if (collect[i][0].ToString()==null|| collect[i][0].ToString() == "")
{
continue;
}
ExcelItem item = new ExcelItem();
//解析每列的数据
item.key = collect[i][0].ToString();
item.Language = new string[4];
item.Language[0] = collect[i][1].ToString();
item.Language[1] = collect[i][2].ToString();
item.Language[2] = collect[i][3].ToString();
item.Language[3] = collect[i][4].ToString();
array.Add(item);
}
return array.ToArray();
}
/// <summary>
/// 读取excel文件内容
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="columnNum">行数</param>
/// <param name="rowNum">列数</param>
/// <returns></returns>
static DataRowCollection ReadExcelContext(string filePath, ref int columnNum, ref int rowNum)
{
FileStream stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
//Debug.Log(stream == null);
IExcelDataReader excelReader = ExcelReaderFactory.CreateOpenXmlReader(stream);
DataSet result = excelReader.AsDataSet();
// Tables[0] 下标0表示excel文件中第一张表的数据
columnNum = result.Tables[0].Columns.Count;
rowNum = result.Tables[0].Rows.Count;
return result.Tables[0].Rows;
}
}
/// <summary>
/// 数据序列化类 -- 和Excel列一一对应
/// </summary>
[System.Serializable]
public class ExcelItem
{
public string key;
public string[] Language;
}
/// <summary>
/// 表数据管理器 -- 存最后的数据集
/// </summary>
public class ExcelManager : ScriptableObject
{
public ExcelItem[] dataArray;
}
ExcelBuild.cs脚本如下
using System.Xml;
using UnityEditor;
// <summary>
/// 编辑器扩展 将xlsx文件转换Xml
/// </summary>
public class ExcelBuild : Editor
{
/// <summary>
/// 转换为 Asset
/// </summary>
[MenuItem("Tools/CreateLocalizationXML")]
public static void CreateLocalizationXML()
{
//创建表格管理器
ExcelManager excelManager = CreateInstance<ExcelManager>();
//获取表格的值
excelManager.dataArray = ExcelTool.CreateItemArrayWithExcel(ExcelConfig.excelsFolderPath);
// 文件保存路径
string[] filePath = new string[excelManager.dataArray[0].Language.Length];
for (int i = 0; i < excelManager.dataArray[0].Language.Length; i++)
{
filePath[i] = ExcelConfig.assetPath(excelManager.dataArray[0].Language[i]);
}
for (int language = 0; language < filePath.Length; language++)
{
XmlDocument xml = new XmlDocument();
//创建声明信息
XmlDeclaration Declaration = xml.CreateXmlDeclaration("1.0", "utf-8", null);
//设置声明信息
xml.AppendChild(Declaration);
//创建根节点
XmlElement Dictionaries = xml.CreateElement("Dictionaries");
// 设置根节点
xml.AppendChild(Dictionaries);
// 一级子节点
XmlElement Dictionary = xml.CreateElement("Dictionary");
Dictionary.SetAttribute("Language", excelManager.dataArray[0].Language[language]);
Dictionaries.AppendChild(Dictionary);
for (int i = 0; i < excelManager.dataArray.Length; i++)
{
//创建数据子节点
XmlElement itemId = xml.CreateElement("String");
itemId.SetAttribute("Key", excelManager.dataArray[i].key);
itemId.SetAttribute("Value", excelManager.dataArray[i].Language[language]);
// 设置节点间关系
Dictionary.AppendChild(itemId);
}
// 保存到本地
xml.Save(filePath[language]);
}
AssetDatabase.Refresh();
}
}
2、实时更新文本显示
功能实现:
1、修改key或切换语言时,实时替换文本内容。
2、修改字体时,替换所有使用同样字体的文本。
3、编辑器创建Text,不用手动挂脚本。
该模块有三个脚本,UIFont.cs,LocalizationText.cs,LocalizationMenuExtension.cs
UIFont.cs字体脚本,脚本做成预制件,预制件只有一个字体属性
脚本内容如下:
using UnityEngine;
public class UIFont : MonoBehaviour {
public Font UseFont;
}
LocalizationText.cs该脚本挂在Text组件所载对象上,主要用途是实时修改text文本显示,和修改字体,设置字体时,只需拖动预制件即可。替换字体时,不用每个text修改,只需修改预制件的字体即可。
代码如下:
using UnityEngine.UI;
using UnityEngine;
using System.Xml;
using UnityGameFramework.Runtime;
namespace StarForce
{
/// <summary>
/// Custom Text Control used for localization text.
/// </summary>
[AddComponentMenu("UI/Text_Local", 10)]
[RequireComponent(typeof(Text))]
public class LocalizationText : MonoBehaviour
{
[HideInInspector]
public Text T;
protected void Awake()
{
T = transform.GetComponent<Text>();
if (CustomFont != null)
{
T.font = CustomFont.UseFont;
}
}
/// <summary>
/// 文本的key
/// </summary>
public string KeyString;
/// <summary>
/// 自定义字体,方便后期替换
/// </summary>
public UIFont CustomFont;
/// <summary>
/// 是否开启自身的本地化
/// </summary>
public bool IsOpenLocalize = true;
/// <summary>
/// 重新本地化,用于游戏内切换语言时调用
/// </summary>
public void OnLocalize()
{
if (IsOpenLocalize)
{
text = GameEntry.Localization.GetString(KeyString);
}
}
public string text
{
get
{
if (IsOpenLocalize)
{
T.text = GameEntry.Localization.GetString(KeyString);
return T.text;
}
else
{
return T.text;
}
}
set
{
if (string.IsNullOrEmpty(value))
{
if (string.IsNullOrEmpty(T.text))
return;
T.text = "";
T.SetVerticesDirty();
}
else
{
T.text = value;
T.SetVerticesDirty();
T.SetLayoutDirty();
}
}
}
#if UNITY_EDITOR
private void OnValidate()
{
if (string.IsNullOrEmpty(KeyString))
{
T.text = KeyString;
}
else
{
string txt = GetKey(KeyString);
if (string.IsNullOrEmpty(txt))
{
T.text ="<NoKey>"+ KeyString;
}
else
{
T.text = txt;
}
if (CustomFont != null)
{
T.font = CustomFont.UseFont;
}
}
}
private string GetKey(string KeyString)
{
string currentLanguage = FindObjectOfType<BaseComponent>().EditorLanguage.ToString();
string dictionaryAssetName = AssetUtility.GetLocalXmlAsset(currentLanguage, "Default", false);
XmlDocument xmlDocument = new XmlDocument();
xmlDocument.Load(dictionaryAssetName);
XmlNode xmlRoot = xmlDocument.SelectSingleNode("Dictionaries");
XmlNodeList xmlNodeDictionaryList = xmlRoot.ChildNodes;
for (int i = 0; i < xmlNodeDictionaryList.Count; i++)
{
XmlNode xmlNodeDictionary = xmlNodeDictionaryList.Item(i);
if (xmlNodeDictionary.Name != "Dictionary")
{
continue;
}
string language = xmlNodeDictionary.Attributes.GetNamedItem("Language").Value;
if (language != currentLanguage)
{
continue;
}
XmlNodeList xmlNodeStringList = xmlNodeDictionary.ChildNodes;
for (int j = 0; j < xmlNodeStringList.Count; j++)
{
XmlNode xmlNodeString = xmlNodeStringList.Item(j);
if (xmlNodeString.Name != "String")
{
continue;
}
string key = xmlNodeString.Attributes.GetNamedItem("Key").Value;
if (key == KeyString)
{
return xmlNodeString.Attributes.GetNamedItem("Value").Value;
}
}
}
return null;
}
#endif
}
}
LocalizationMenuExtension.cs编辑器创建Text_Local文本对象
public class LocalizationMenuExtension : MonoBehaviour {
public const string kUILayerName = "UI";
[MenuItem("GameObject/UI/Text_Local", false, 2000)]
static public void AddText(MenuCommand menuCommand)
{
GameObject go = new GameObject("Text");
LocalizationText txt = go.AddComponent<LocalizationText>();
InitValue(txt);
}
/// <summary>
/// 初始化值为了和Text的初始值保持一致
/// </summary>
/// <param name="txt"></param>
public static void InitValue(LocalizationText txt)
{
txt.T.color = new Color(50f / 255f, 50f / 255f, 50f / 255f);
RectTransform contentRT = txt.GetComponent<RectTransform>();
contentRT.sizeDelta = new Vector2(160f, 30f);
txt.gameObject.layer = LayerMask.NameToLayer(kUILayerName);
}
}
3、框架辅助修改
UGuiForm方法OnInit初始化中读取的text,直接修改文本,加了本地化脚本后,改为直接读取LocalizationText脚本。
protected override void OnInit(object userData)
{
base.OnInit(userData);
m_CachedCanvas = gameObject.GetOrAddComponent<Canvas>();
m_CachedCanvas.overrideSorting = true;
OriginalDepth = m_CachedCanvas.sortingOrder;
m_CanvasGroup = gameObject.GetOrAddComponent<CanvasGroup>();
RectTransform transform = GetComponent<RectTransform>();
transform.anchorMin = Vector2.zero;
transform.anchorMax = Vector2.one;
transform.anchoredPosition = Vector2.zero;
transform.sizeDelta = Vector2.zero;
gameObject.GetOrAddComponent<GraphicRaycaster>();
LocalizationText[] texts = GetComponentsInChildren<LocalizationText>(true);
for (int i = 0; i < texts.Length; i++)
{
if (!string.IsNullOrEmpty(texts[i].KeyString))
{
texts[i].text = GameEntry.Localization.GetString(texts[i].KeyString);
}
}
}
注意,UI预制件编辑时,专门放在一个场景里。修改编辑器模式下的语言配置,对应的场景中的UI预制件都会更新文本显示。所用到的UI预制件需放到场景中。