美文网首页Unity 学习路径
Unity实践—本地化框架实现

Unity实践—本地化框架实现

作者: Warl_G | 来源:发表于2020-06-08 02:20 被阅读0次

    基于 Unity 现有文件管理系统实现的本地化框架,代码已上传 Git 项目 GRUnityTools,可直接下载源码或通过 UPM 使用

    现已实现 Resources、AssetBundle 和 Addressable 资源管理方式,另外可根据需求自由扩展

    提供逐行解析,csv 解析和 json 解析,三种本地化文本解析方式,也可根据需求自由扩展

    本文原地址:Unity实践—本地化框架实现

    实现目标

    1. 支持语言的可扩展性
    2. 加载方式的可扩展性
    3. 文本解析的可扩展性
    4. 动态加载本地化资源
    5. 快速的资源更新方式

    实现方式

    1. SystemLanguage

      为确保不同的项目和开发对象的本地化内容的规范统一,直接使用 SystemLanguage 枚举型作为各种语言的唯一ID

    2. ILocalizationLoader

      通过自定义接口的形式,将加载方法开放给开发者实现,使不同项目实现最大自由地定制

    3. ILocalizationParser

      通过自定义接口的形式,将文本解析方法开放给开发者实现,使不同项目实现最大自由地定制

    4. Resources,AssetBundle 和 Addressable

      使用 Unity 提供的三种动态加载资源方法实现ILocalizationLoader接口,提供三种预设本地化资源动态加载器

    5. LocalizationComponent

      为进一步简化开发过程,提供该脚本实现切换语言时本地化控件的自动批量更新,支持 Text、TextMesh、Image 和 SpriteRender

    项目结构

    • LocalizationManager
      • ILocalizationLoader
        • LocalizationResourcesLoader
        • LocalizationAssetBundleLoader
        • LocalizationAddressableLoader
        • ...LocalizationCustomLoader
      • IlocalizationParser
        • LocalizationDefaultParser
        • ...LocalizationCustomParser
    • LocalizationComponent
      • LocalizationComponentItem

    实现过程

    为节省篇幅,部分示例代码有删改,具体代码可见项目 GRUnityTools,并包含使用示例

    LocalizationManager

    最初设计时,仅为实现本地化文本加载与切换的简单功能,因此所有的加载和解析功能均由 LocalizationManager 单例完成

    首先思考的问题是如何使不同项目对拥有同一套语言表,由我个人补充难免会有缺漏,无法覆盖所有需求,最终决定使用系统提供的枚举型SystemLanguage,基本涵盖了所有应用度比较广的语言,同时确认以枚举名称作为本地化文本文件名称用于加载,可加入序号作为语言顺序

    构建文件数据结构,用于匹配语言加载文件

    public struct LocalizationFile
    {
        public int Index { get; }
        public SystemLanguage Type { get; }
        public string Name  { get; }
        public string FileName  { get; }
            public LocalizationFile(string fileName = null)
        {   
            if (!string.IsNullOrEmpty(fileName))
            {
                Name = fileName;
                string[] lan = fileName.Split('.');
                Name = lan[1];
                bool success;
                int index;
                SystemLanguage lanuageType;
                success = Int32.TryParse(lan[0], out index);
                Index = success ? index : -1;
                success = Enum.TryParse(Name, true, out lanuageType);
                Type = success ? lanuageType : SystemLanguage.Unknown;
            }
        }
    }
    

    LocalizatioManager 单例初始化时,使用 Resources 获取规定目录下所有文本文件以获取支持的语言列表

    TextAsset[] res = Resources.LoadAll<TextAsset>("Localization");
    List<LocalizationFile> fileList = new List<LocalizationFile>();
    
    for (int i = 0; i < res.Length; i++)
    {
        TextAsset asset = res[i];
        LocalizationFile data = new LocalizationFile(asset.name);
        fileList.Add(data);
        Resources.UnloadAsset(asset);
    }
    

    使用 Resources 提供的接口加载规定子目录下的其他本地化资源

    Resources.LoadAsync<T>("Localization/Assets/" + assetPath);
    

    最初使用的自定义的文本规则的 txt 文件,每行一个键值对,等号分隔键和值,使用Regex.Unescape方法将文本中的转义符转义,LocalizationManager 持有解析后的字典数据,供外部控件使用

    public Dictionary<string, string> ParseTxt(string txt)
    {
        if (string.IsNullOrEmpty(txt))
        {
            return null;
        }
        string[] lines = txt.Split('\n');
        Dictionary<string, string> localDict = new Dictionary<string, string>();
        foreach (string line in lines)
        {
            if (!string.IsNullOrEmpty(line))
            {
                string[] keyAndValue = line.Split(new[] {'='}, 2);
                if (keyAndValue.Length == 2)
                {
                    string value = Regex.Unescape(keyAndValue[1]);
                    localDict.Add(keyAndValue[0], value);
                }
            }
        }
    
        return localDict;
    }
    

    ILocalizationLoader

    由于 Resources自身的弊端和限制,需要使用更加灵活的资源动态加载方法,因此又加入了对 AssetBundle 和 Addressable 的支持,有关这两种方式的信息可看文章Unity学习—资源管理概览

    在设计这两种加载方式时意识到,提供现成的加载方法会有大量的约束限制,无法做到通用,因此决定抽离 LocalizationManager 所需的两个固定的方法作为接口,以实现外部自定义

    public interface ILocalizationLoader
    { 
        //用于加载支持语言列表
        void LoadAllFileListAsync(Action<LocalizationFile[]> complete);
        //用于加载本地化资源
        void LoadAssetAsync<T>(string localizationFileName, string assetPath, bool defaultAsset, Action<T> complete) where T : Object;
    }
    

    首先将之前的 Resources 加载方式改写为实现该接口的LocalizationResourcesLoader,并扩展了LocalizationAssetBundleLoaderLocalizationAddressableLoader两个加载器

    LocalizationAssetBundleLoader

    将不同语言的 AssetBundle 打包到 Assets/StreamingAssets/Localization 目录下,并使用类似于Resources的资源命名方式命名,加载 Localization 的 manifest 获取支持语言的 Bundle 清单

    string bundlePath = Path.Combine(Application.streamingAssetsPath, FilesPath, FilesPath);
    //加载自动生成的 Localization Bundle
    var loadrequest = AssetBundle.LoadFromFileAsync("Assets/StreamingAssets/Localization/Localization");
    loadrequest.completed += operation =>
    {
        //获取 Localization Bundle 的 Manifest 文件信息
        AssetBundleManifest manifest =
            loadrequest.assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
        var files = manifest.GetAllAssetBundles();
    
        List<LocalizationFile> fileList = new List<LocalizationFile>();
        for (int i = 0; i < files.Length; i++)
        {
            if (!files[i].Equals(CommonAssetsPath.ToLower()))
            {
                LocalizationFile data = new LocalizationFile(files[i]);
                fileList.Add(data);
            }
        }
    
        loadrequest.assetBundle.Unload(manifest);
    }
    

    加载资源

    AssetBundleCreateRequest loadrequest = AssetBundle.LoadFromFileAsync(Path.Combine(Application.streamingAssetsPath, FilesPath,
                        localizationFileName));
    loadrequest.completed += operation =>
    {
        var request = loadrequest.assetBundle.LoadAssetAsync<T>(assetPath);
        request.completed += asyncOperation => 
        { 
            //get request.asset
        };
    };  
    

    同时为更便捷打包本地化 AssetBundle 包,增加快捷方法

    string[] paths = Directory.GetDirectories(localizationFilePath);
    AssetBundleBuild[] buildMap = new AssetBundleBuild[paths.Length];
    
    for (int i = 0; i < paths.Length; i++)
    {
        string bundleName = Path.GetFileName(paths[i]);
        buildMap[i].assetBundleName = bundleName;
        var files = Directory.GetFiles(paths[i], "*", SearchOption.AllDirectories);
    
        List<string> assets = new List<string>();
        foreach (var file in files)
        {
            if (!file.EndsWith(".meta"))
            {
                var filePath = file.Replace(Application.dataPath, "Assets");
                assets.Add(filePath);
            }
        }
    
        buildMap[i].assetNames = assets.ToArray();
    }
    
    var parent = Path.GetFileName(localizationFilePath);
    BuildPipeline.BuildAssetBundles("Assets/StreamingAssets/" + parent, buildMap,
                        BuildAssetBundleOptions.ChunkBasedCompression, target);
    

    LocalizationAddressableLoader

    由于Addressable的特性,无法通过固定的资源路径获取文件获取支持语言,因此使用 AddressableLabel功能,给每种语言的本地化文本文件打上Localization标签,实现统一获取,且需将文本文件 address 与对应SystemLanguage相同

    Addressables.LoadResourceLocationsAsync(KLocalizeFolder).Completed += handle =>
    {
        List<LocalizationFile> fileList = new List<LocalizationFile>();
        if (handle.Status == AsyncOperationStatus.Succeeded)
        {
            foreach (var file in handle.Result)
            {
                LocalizationFile data = new LocalizationFile(file.PrimaryKey);
                fileList.Add(data);
            }
        }
    };
    

    使用 Addressables 加载资源

    Addressables.LoadAssetAsync<T>(assetAddress).Completed += handle =>
    {
        //get request.asset
    };
    

    ILocalizationParser

    由于编写的文本解析格式过于个性化,并不通用,因此扩展支持了 csv 和 json 的解析,同时仿照ILocalizationLoader方式,抽出解析方法到ILocalizationParser,以增加扩展性

    public interface ILocalizationParser
    {
        Dictionary<string, string> Parse(string text);
    }
    

    LocalizationDefaultParser

    项目将上述的 txt、csv 和 json 解析方法汇总到 LocalizationDefaultParser中,通过LocalizationFileType决定解析方式

    LocalizationComponent

    LocalizationComponent 是一套用于挂载在场景对象的脚本,用于自动更新本地化文本和资源,使用LocalizationComponentItem在 Inspector 中将 Component匹配本地化键

    [Serializable]
    public class LocalizationComponentItem
    {
        [SerializeField] internal Component component;
        public string localizationKey;
        public string defaultValue;
        public bool setNativeSize;
        internal Vector2 originalImageSize = Vector2.zero;
    }
    
    public class LocalizationComponent : MonoBehaviour
    {
            public LocalizationComponentItem[] items; 
                private void LocalizationChanged(LocalizationFile localizationFile)
            {
                foreach (var item in items)
                {
                    if (!string.IsNullOrEmpty(item.localizationKey) && item.component != null)
                    {
                        string value = LocalizationManager.Singleton.GetLocalizedText(item.localizationKey);
                        if (value == null)
                        {
                            value = item.defaultValue;
                        }
    
                        if (item.component is Text text)
                        {
                            text.text = value;
                        }
                        else if (item.component is TextMesh mesh)
                        {
                            mesh.text = value;
                        }
                        else
                        {
                            Image image = null;
                            SpriteRenderer spriteRenderer = null;
    
                            if (item.component is Image imageComponent)
                            {
                                image = imageComponent;
                                if (item.originalImageSize == Vector2.zero)
                                {
                                    item.originalImageSize = image.rectTransform.sizeDelta;
                                }
                            }
    
                            if (item.component is SpriteRenderer rendererComponent)
                            {
                                spriteRenderer = rendererComponent;
                            }
    
                            if (image != null || spriteRenderer != null)
                            {
                                //替换图片
                                LocalizationManager.Singleton.LoadLocalizationAssetAsync(value, item.defaultValue,
                                    delegate(Sprite sprite)
                                    {
                                        if (sprite != null)
                                        {
                                            if (image != null)
                                            {
                                                Image img = (Image) item.component;
                                                img.sprite = sprite;
                                                if (item.setNativeSize)
                                                {
                                                    img.SetNativeSize();
                                                }
                                                else
                                                {
                                                    image.rectTransform.sizeDelta = item.originalImageSize;
                                                }
                                            }
    
                                            if (spriteRenderer != null)
                                            {
                                                spriteRenderer.sprite = sprite;
                                            }
                                        }
                                    });
                            }
                        }
                    }
                }
            }
      
    }
    

    相关文章

      网友评论

        本文标题:Unity实践—本地化框架实现

        本文链接:https://www.haomeiwen.com/subject/couktktx.html