簡介
之前搗鼓過一個通過csv配置游戲多語言支持的小工具,但是發現使用過程中,通過notepad++去進行轉碼很不方便,並且直接將配置的csv不加密的放在游戲中心里感覺不是很踏實
於是乎~~
新的方案
1.在PC/MAC平台上解析多語言配置,也就是editor運行環境中解析csv或者excel
2.通過在Editor運行過程中生成多個語言對象,然后序列化並加密存盤
3.在使用端(移動端)通過resources加載加密之后的文件
4.讀取對應的語言序列化文件並實例化加載到游戲中進行使用
使用方法
由於不想讓csv文件被打包入游戲工程中,所以選擇與Assets文件夾並列的路徑文件:
PS:當然路徑可以自己指定

然后里面存放多語言csv

然后代碼中是調用是這樣的:

代碼實現
序列化反序列化工具
using Newtonsoft.Json;
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
public static class SaveHelper
{
private const string M_KEY = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
public static bool IsFileExist(string filePath)
{
return File.Exists(filePath);
}
public static bool IsDirectoryExists(string filePath)
{
return Directory.Exists(filePath);
}
public static void CreateFile(string fileName, string content)
{
StreamWriter streamWriter = File.CreateText(fileName);
streamWriter.Write(content);
streamWriter.Close();
}
public static void CreateDirectory(string filePath)
{
if (IsDirectoryExists(filePath))
{
return;
}
Directory.CreateDirectory(filePath);
}
private static string SerializeObject(object pObject)
{
string serializedString = string.Empty;
serializedString = JsonConvert.SerializeObject(pObject);
return serializedString;
}
private static object DeserializeObject(string pString, Type pType)
{
object deserializedObject = null;
deserializedObject = JsonConvert.DeserializeObject(pString, pType);
return deserializedObject;
}
private static string RijndaelEncrypt(string pString, string pKey)
{
byte[] keyArray = UTF8Encoding.UTF8.GetBytes(pKey);
byte[] toEncryptArray = UTF8Encoding.UTF8.GetBytes(pString);
RijndaelManaged rDel = new RijndaelManaged();
rDel.Key = keyArray;
rDel.Mode = CipherMode.ECB;
rDel.Padding = PaddingMode.PKCS7;
ICryptoTransform cTransform = rDel.CreateEncryptor();
byte[] resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length);
return Convert.ToBase64String(resultArray, 0, resultArray.Length);
}
private static String RijndaelDecrypt(string pString, string pKey)
{
byte[] keyArray = UTF8Encoding.UTF8.GetBytes(pKey);
byte[] toEncryptArray = Convert.FromBase64String(pString);
RijndaelManaged rDel = new RijndaelManaged();
rDel.Key = keyArray;
rDel.Mode = CipherMode.ECB;
rDel.Padding = PaddingMode.PKCS7;
ICryptoTransform cTransform = rDel.CreateDecryptor();
byte[] resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length);
return UTF8Encoding.UTF8.GetString(resultArray);
}
public static void SaveData(string fileName, object pObject)
{
// 如果文件已存在,則刪除
if (File.Exists(fileName))
{
File.Delete(fileName);
}
string toSave = SerializeObject(pObject);
toSave = RijndaelEncrypt(toSave, M_KEY);
StreamWriter streamWriter = File.CreateText(fileName);
streamWriter.Write(toSave);
streamWriter.Close();
}
public static object ReadData(string str, Type pType, bool isFile = true)
{
string data;
if (isFile)
{
// 如果文件不存在,則返回空
if (!File.Exists(str))
{
return null;
}
StreamReader streamReader = File.OpenText(str);
data = streamReader.ReadToEnd();
streamReader.Close();
}
else
{
data = str;
}
data = RijndaelDecrypt(data, M_KEY);
return DeserializeObject(data, pType);
}
}
CSV解析器
using System.Collections.Generic;
using System.IO;
using System.Text;
// col是豎行,row是橫排,防止我忘了
public class LTCSVLoader
{
private TextReader inStream = null;
private List<string> vContent;
private List<List<string>> table;
/// <summary>
/// 只支持GBK2312的編碼(WPS直接保存的編碼支持,僅提供給Windows使用)
/// </summary>
/// <param name="fileName"></param>
public void ReadFile(string fileName)
{
inStream = new StreamReader(fileName, Encoding.GetEncoding("GBK"));
table = new List<List<string>>();
List<string> temp = this.getLineContentVector();
while (null != temp)
{
List<string> tempList = new List<string>();
for (int i = 0; i < temp.Count; ++i)
{
tempList.Add(temp[i]);
}
table.Add(tempList);
temp = this.getLineContentVector();
}
}
/// <summary>
/// 目前只支持UTF-8的編碼(WPS直接保存的編碼不支持)
/// </summary>
/// <param name="str"></param>
public void ReadMultiLine(string str)
{
inStream = new StringReader(str);
table = new List<List<string>>();
List<string> temp = this.getLineContentVector();
while (null != temp)
{
List<string> tempList = new List<string>();
for (int i = 0; i < temp.Count; ++i)
{
tempList.Add(temp[i]);
}
table.Add(tempList);
temp = this.getLineContentVector();
}
}
private int containsNumber(string parentStr, string parameter)
{
int containNumber = 0;
if (parentStr == null || parentStr.Equals(""))
{
return 0;
}
if (parameter == null || parameter.Equals(""))
{
return 0;
}
for (int i = 0; i < parentStr.Length; i++)
{
i = parentStr.IndexOf(parameter, i);
if (i > -1)
{
i = i + parameter.Length;
i--;
containNumber = containNumber + 1;
}
else
{
break;
}
}
return containNumber;
}
private bool isQuoteAdjacent(string p_String)
{
bool ret = false;
string temp = p_String;
temp = temp.Replace("\"\"", "");
if (temp.IndexOf("\"") == -1)
{
ret = true;
}
return ret;
}
private bool isQuoteContained(string p_String)
{
bool ret = false;
if (p_String == null || p_String.Equals(""))
{
return false;
}
if (p_String.IndexOf("\"") > -1)
{
ret = true;
}
return ret;
}
private string[] readAtomString(string lineStr)
{
string atomString = "";// 要讀取的原子字符串
string orgString = "";// 保存第一次讀取下一個逗號時的未經任何處理的字符串
string[] ret = new string[2];// 要返回到外面的數組
bool isAtom = false;// 是否是原子字符串的標志
string[] commaStr = lineStr.Split(new char[] { ',' });
while (!isAtom)
{
foreach (string str in commaStr)
{
if (!atomString.Equals(""))
{
atomString = atomString + ",";
}
atomString = atomString + str;
orgString = atomString;
if (!isQuoteContained(atomString))
{
// 如果字符串中不包含引號,則為正常,返回
isAtom = true;
break;
}
else
{
if (!atomString.StartsWith("\""))
{
// 如果字符串不是以引號開始,則表示不轉義,返回
isAtom = true;
break;
}
else if (atomString.StartsWith("\""))
{
// 如果字符串以引號開始,則表示轉義
if (containsNumber(atomString, "\"") % 2 == 0)
{
// 如果含有偶數個引號
string temp = atomString;
if (temp.EndsWith("\""))
{
temp = temp.Replace("\"\"", "");
if (temp.Equals(""))
{
// 如果temp為空
atomString = "";
isAtom = true;
break;
}
else
{
// 如果temp不為空,則去掉前后引號
temp = temp.Substring(1, temp.LastIndexOf("\""));
if (temp.IndexOf("\"") > -1)
{
// 去掉前后引號和相鄰引號之后,若temp還包含有引號
// 說明這些引號是單個單個出現的
temp = atomString;
temp = temp.Substring(1);
temp = temp.Substring(0, temp.IndexOf("\""))
+ temp.Substring(temp.IndexOf("\"") + 1);
atomString = temp;
isAtom = true;
break;
}
else
{
// 正常的csv文件
temp = atomString;
temp = temp.Substring(1, temp.LastIndexOf("\""));
temp = temp.Replace("\"\"", "\"");
atomString = temp;
isAtom = true;
break;
}
}
}
else
{
// 如果不是以引號結束,則去掉前兩個引號
temp = temp.Substring(1, temp.IndexOf('\"', 1))
+ temp.Substring(temp.IndexOf('\"', 1) + 1);
atomString = temp;
isAtom = true;
break;
}
}
else
{
// 如果含有奇數個引號
if (!atomString.Equals("\""))
{
string tempAtomStr = atomString.Substring(1);
if (!isQuoteAdjacent(tempAtomStr))
{
// 這里做的原因是,如果判斷前面的字符串不是原子字符串的時候就讀取第一個取到的字符串
// 后面取到的字符串不計入該原子字符串
tempAtomStr = atomString.Substring(1);
int tempQutoIndex = tempAtomStr.IndexOf("\"");
// 這里既然有奇數個quto,所以第二個quto肯定不是最后一個
tempAtomStr = tempAtomStr.Substring(0, tempQutoIndex)
+ tempAtomStr.Substring(tempQutoIndex + 1);
atomString = tempAtomStr;
isAtom = true;
break;
}
}
}
}
}
}
}
// 先去掉之前讀取的原字符串的母字符串
if (lineStr.Length > orgString.Length)
{
lineStr = lineStr.Substring(orgString.Length);
}
else
{
lineStr = "";
}
// 去掉之后,判斷是否以逗號開始,如果以逗號開始則去掉逗號
if (lineStr.StartsWith(","))
{
if (lineStr.Length > 1)
{
lineStr = lineStr.Substring(1);
}
else
{
lineStr = "";
}
}
ret[0] = atomString;
ret[1] = lineStr;
return ret;
}
private bool readCSVNextRecord()
{
// 如果流未被初始化則返回false
if (inStream == null)
{
return false;
}
// 如果結果向量未被初始化,則初始化
if (vContent == null)
{
vContent = new List<string>();
}
// 移除向量中以前的元素
vContent.Clear();
// 聲明邏輯行
string logicLineStr = "";
// 用於存放讀到的行
StringBuilder strb = new StringBuilder();
// 聲明是否為邏輯行的標志,初始化為false
bool isLogicLine = false;
while (!isLogicLine)
{
string newLineStr = inStream.ReadLine();
if (newLineStr == null)
{
strb = null;
vContent = null;
isLogicLine = true;
break;
}
if (newLineStr.StartsWith("#"))
{
// 去掉注釋
continue;
}
if (!strb.ToString().Equals(""))
{
strb.Append("\r\n");
}
strb.Append(newLineStr);
string oldLineStr = strb.ToString();
if (oldLineStr.IndexOf(",") == -1)
{
// 如果該行未包含逗號
if (containsNumber(oldLineStr, "\"") % 2 == 0)
{
// 如果包含偶數個引號
isLogicLine = true;
break;
}
else
{
if (oldLineStr.StartsWith("\""))
{
if (oldLineStr.Equals("\""))
{
continue;
}
else
{
string tempOldStr = oldLineStr.Substring(1);
if (isQuoteAdjacent(tempOldStr))
{
// 如果剩下的引號兩兩相鄰,則不是一行
continue;
}
else
{
// 否則就是一行
isLogicLine = true;
break;
}
}
}
}
}
else
{
// quotes表示復數的quote
string tempOldLineStr = oldLineStr.Replace("\"\"", "");
int lastQuoteIndex = tempOldLineStr.LastIndexOf("\"");
if (lastQuoteIndex == 0)
{
continue;
}
else if (lastQuoteIndex == -1)
{
isLogicLine = true;
break;
}
else
{
tempOldLineStr = tempOldLineStr.Replace("\",\"", "");
lastQuoteIndex = tempOldLineStr.LastIndexOf("\"");
if (lastQuoteIndex == 0)
{
continue;
}
if (tempOldLineStr[lastQuoteIndex - 1] == ',')
{
continue;
}
else
{
isLogicLine = true;
break;
}
}
}
}
if (strb == null)
{
// 讀到行尾時為返回
return false;
}
// 提取邏輯行
logicLineStr = strb.ToString();
if (logicLineStr != null)
{
// 拆分邏輯行,把分離出來的原子字符串放入向量中
while (!logicLineStr.Equals(""))
{
string[] ret = readAtomString(logicLineStr);
string atomString = ret[0];
logicLineStr = ret[1];
vContent.Add(atomString);
}
}
return true;
}
private List<string> getLineContentVector()
{
if (this.readCSVNextRecord())
{
return this.vContent;
}
return null;
}
private List<string> getVContent()
{
return this.vContent;
}
public int GetRow()
{
if (null == table)
{
throw new System.Exception("table尚未初始化,請檢查是否成功讀取");
}
return table.Count;
}
public int GetCol()
{
if (null == table)
{
throw new System.Exception("table尚未初始化,請檢查是否成功讀取");
}
if (table.Count == 0)
{
throw new System.Exception("table內容為空");
}
return table[0].Count;
}
public int GetFirstIndexAtCol(string str, int col)
{
if (null == table)
{
throw new System.Exception("table尚未初始化,請檢查是否成功讀取");
}
if (table.Count == 0)
{
throw new System.Exception("table內容為空");
}
if (col >= table[0].Count)
{
throw new System.Exception("參數錯誤:col大於最大行");
}
for (int i = 0; i < table.Count; ++i)
{
if (table[i][col].Equals(str))
{
return i;
}
}
return -1;
}
public int GetFirstIndexAtRow(string str, int row)
{
if (null == table)
{
throw new System.Exception("table尚未初始化,請檢查是否成功讀取");
}
if (table.Count == 0)
{
throw new System.Exception("table內容為空");
}
if (row >= table.Count)
{
throw new System.Exception("參數錯誤:cow大於最大列");
}
int tempCount = table[0].Count;
for (int i = 0; i < tempCount; ++i)
{
if (table[row][i].Equals(str))
{
return i;
}
}
return -1;
}
public int[] GetIndexsAtCol(string str, int col)
{
if (null == table)
{
throw new System.Exception("table尚未初始化,請檢查是否成功讀取");
}
if (table.Count == 0)
{
throw new System.Exception("table內容為空");
}
if (col >= table[0].Count)
{
throw new System.Exception("參數錯誤:col大於最大行");
}
List<int> tempList = new List<int>();
for (int i = 0; i < table.Count; ++i)
{
if (table[i][col].Equals(str))
{
// 增加
tempList.Add(i);
}
}
return tempList.ToArray();
}
public int[] GetIndexsAtRow(string str, int row)
{
if (null == table)
{
throw new System.Exception("table尚未初始化,請檢查是否成功讀取");
}
if (table.Count == 0)
{
throw new System.Exception("table內容為空");
}
if (row >= table.Count)
{
throw new System.Exception("參數錯誤:cow大於最大列");
}
int tempCount = table[0].Count;
List<int> tempList = new List<int>();
for (int i = 0; i < tempCount; ++i)
{
if (table[row][i].Equals(str))
{
tempList.Add(i);
}
}
return tempList.ToArray();
}
public string GetValueAt(int col, int row)
{
if (null == table)
{
throw new System.Exception("table尚未初始化,請檢查是否成功讀取");
}
if (table.Count == 0)
{
throw new System.Exception("table內容為空");
}
if (row >= table.Count)
{
throw new System.Exception("參數錯誤:row大於最大列");
}
if (col >= table[0].Count)
{
throw new System.Exception("參數錯誤:col大於最大行");
}
return table[row][col];
}
}
多語言實現類
using UnityEngine;
using System.Collections.Generic;
public class LTLocalization
{
public const string LANGUAGE_ENGLISH = "EN";
public const string LANGUAGE_CHINESE = "CN";
public const string LANGUAGE_JAPANESE = "JP";
public const string LANGUAGE_FRENCH = "FR";
public const string LANGUAGE_GERMAN = "GE";
public const string LANGUAGE_ITALY = "IT";
public const string LANGUAGE_KOREA = "KR";
public const string LANGUAGE_RUSSIA = "RU";
public const string LANGUAGE_SPANISH = "SP";
private const string KEY_CODE = "KEY";
private const string FILE_PATH = "LTLocalization/localization";
private SystemLanguage language = SystemLanguage.Chinese;
private Dictionary<string, string> textData = new Dictionary<string, string>();
private static LTLocalization mInstance;
private LTLocalization()
{
}
private static string GetLanguageAB(SystemLanguage language)
{
switch (language)
{
case SystemLanguage.Afrikaans:
case SystemLanguage.Arabic:
case SystemLanguage.Basque:
case SystemLanguage.Belarusian:
case SystemLanguage.Bulgarian:
case SystemLanguage.Catalan:
return LANGUAGE_ENGLISH;
case SystemLanguage.Chinese:
case SystemLanguage.ChineseTraditional:
case SystemLanguage.ChineseSimplified:
return LANGUAGE_CHINESE;
case SystemLanguage.Czech:
case SystemLanguage.Danish:
case SystemLanguage.Dutch:
case SystemLanguage.English:
case SystemLanguage.Estonian:
case SystemLanguage.Faroese:
case SystemLanguage.Finnish:
return LANGUAGE_ENGLISH;
case SystemLanguage.French:
return LANGUAGE_FRENCH;
case SystemLanguage.German:
return LANGUAGE_GERMAN;
case SystemLanguage.Greek:
case SystemLanguage.Hebrew:
case SystemLanguage.Icelandic:
case SystemLanguage.Indonesian:
return LANGUAGE_ENGLISH;
case SystemLanguage.Italian:
return LANGUAGE_ITALY;
case SystemLanguage.Japanese:
return LANGUAGE_JAPANESE;
case SystemLanguage.Korean:
return LANGUAGE_KOREA;
case SystemLanguage.Latvian:
case SystemLanguage.Lithuanian:
case SystemLanguage.Norwegian:
case SystemLanguage.Polish:
case SystemLanguage.Portuguese:
case SystemLanguage.Romanian:
return LANGUAGE_ENGLISH;
case SystemLanguage.Russian:
return LANGUAGE_RUSSIA;
case SystemLanguage.SerboCroatian:
case SystemLanguage.Slovak:
case SystemLanguage.Slovenian:
return LANGUAGE_ENGLISH;
case SystemLanguage.Spanish:
return LANGUAGE_SPANISH;
case SystemLanguage.Swedish:
case SystemLanguage.Thai:
case SystemLanguage.Turkish:
case SystemLanguage.Ukrainian:
case SystemLanguage.Vietnamese:
case SystemLanguage.Unknown:
return LANGUAGE_ENGLISH;
}
return LANGUAGE_CHINESE;
}
private static string GetWinReadPath(string fileName)
{
return Application.dataPath + "/../" + fileName + ".csv";
}
private static string GetWinSavePath(string fileName)
{
return Application.dataPath + "/Resources/LTLocalization/" + fileName + ".txt";
}
private void ReadData()
{
#if UNITY_EDITOR
// 在Windows平台下讀取語言配置文件
string CSVFilePath = GetWinReadPath(FILE_PATH);
LTCSVLoader loader = new LTCSVLoader();
loader.ReadFile(CSVFilePath);
// 將配置文件序列化為多個語言類
int csvRow = loader.GetRow();
int csvCol = loader.GetCol();
Debug.Log("row:" + csvRow + "col:" + csvCol);
for (int tempCol = 1; tempCol < csvCol; ++tempCol)
{
LTLocalizationData languageData = new LTLocalizationData();
// 獲取第一行數據(語言類型)
languageData.LanguageType = loader.GetValueAt(tempCol, 0);
// 遍歷生成變量
languageData.LanguageData = new Dictionary<string, string>();
for (int tempRow = 1; tempRow < csvRow; ++tempRow)
{
languageData.LanguageData.Add(loader.GetValueAt(0, tempRow), loader.GetValueAt(tempCol, tempRow));
}
// 將語言對象序列化存檔
SaveHelper.SaveData(GetWinSavePath(languageData.LanguageType), languageData);
if (GetLanguageAB(language).Equals(languageData.LanguageType))
{
textData = languageData.LanguageData;
}
}
#else
// 讀取對應的語言對象
TextAsset tempAsset = (TextAsset)Resources.Load("LTLocalization/" + GetLanguageAB(language), typeof(TextAsset));
if (null == tempAsset)
{
tempAsset = (TextAsset)Resources.Load("LTLocalization/" + "EN", typeof(TextAsset));
}
if (null == tempAsset)
{
Debug.LogError("未檢測到語言配置文件");
}
else
{
string saveData = tempAsset.text;
LTLocalizationData currentLanguageData = (LTLocalizationData)SaveHelper.ReadData(saveData, typeof(LTLocalizationData), false);
textData = currentLanguageData.LanguageData;
}
#endif
}
private void SetLanguage(SystemLanguage language)
{
this.language = language;
}
public static void Init()
{
mInstance = new LTLocalization();
mInstance.SetLanguage(Application.systemLanguage);
mInstance.ReadData();
}
public static void ManualSetLanguage(SystemLanguage setLanguage)
{
if (null == mInstance)
{
mInstance = new LTLocalization();
}
mInstance.SetLanguage(setLanguage);
mInstance.ReadData();
}
public static string GetText(string key)
{
if (null == mInstance)
{
Init();
}
if (mInstance.textData.ContainsKey(key))
{
return mInstance.textData[key];
}
return "[NoDefine]" + key;
}
}
多語言數據對象
using UnityEngine;
using System.Collections.Generic;
public class LTLocalizationData
{
public string LanguageType;
public Dictionary<string, string> LanguageData;
public override string ToString()
{
string result = "LanguageType:" + LanguageType;
List<string> tempKeys = new List<string>(LanguageData.Keys);
for (int i = 0; i < tempKeys.Count; ++i)
{
result += "\nKey:[" + tempKeys[i] + "]|Value:[" + LanguageData[tempKeys[i]] + "]";
}
return result;
}
}
總結
感覺比上一個版本好了很多
1.不用考慮csv編碼的問題了
2.反序列化速度比讀取csv更快
3.加了密更可靠
4.不同語言分開讀取,占用內存更小
缺點嘛,暫時覺得還不錯~~繼續先用着,有問題再改
