Unity杂文——海外开发踩坑笔记

原文地址

打包

Gradle版本问题

本地打包的大部分错误都是因为这个问题,这是因为笔者接入的SDK自定义了gradle的插件版本,这个再unity本身其实已经定义过了,但是自己是可以通过修改build.gradle进行修改的。查看unity本身gradel的插件版本的路径是:Editor\Data\PlaybackEngines\AndroidPlayer\Tools\GradleTemplates\baseProjectTemplate.gradle,以2019.4.28版本为例,里面代码为:

// GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN
allprojects {
    buildscript {
        ***
        dependencies {
            ***
            classpath 'com.android.tools.build:gradle:3.4.0'
            **BUILD_SCRIPT_DEPS**
        }
    }
    ***
}
***

如上图,可以看到插件版本为3.4.0,如果unity里本身修改了baseProjectTemplate.gradle就按照修改后的来,这个文件夹内所有的gradle和properties都是默认的,如果程序里修改就按照程序里的来。修改的方法在Editor–>ProjectSettings–>Player–>Publishing Settings,如下图所示:

image.png

如上图,其实就是对应编辑器文件夹下的gradle文件,如果打勾就会在pluging/Android文件夹下生成对应的文件,就可以直接修改,不再按照unity默认的来,就可以修改配置了。

经过上面的介绍已经知道如何查看并修改unity的gradle插件版本,下面就是修改对应的gradle版本。首先打开Editor–>Preference–>External Tools就可以看到Android的打包环境配置。

image.png

2020版本以后的Unity是默认路径下就自己配置好环境,选择默认就可以,但是依旧可能会存在环境不存在或者版本不对,所以可以自己配置,这样修改也方便。这里有需要特别关注的一点也是大部分打包失败的原因,就是gradle的版本和对应的插件版本是有对照关系的,必须对照上才能正常打包。对应关系如下图:

image.png

只要配置好对应的关系就行了。

NDK版本问题

在打包的时候也遇到了NDK版本不对无法打包的问题,打包失败会提示打包需要的版本,下载对应的版本即可,笔者打包的时候需要的是版本19,但是下载19版本依旧无法打包,这是因为版本的小版本依旧对不上,这里可以不用找对应的小版本,只要对应的大版本一样,在自己ndk安装目录下,找到source.properties文件,编辑文件,如下,修改对应的Pkg.Revision即可。

Pkg.Desc = Android NDK
Pkg.Revision = 19.0.5232133

maven仓库下载问题

这个问题是打包的时候并没有找到对应的maven仓库,笔者接入的SDK需要的maven都写在了launcherTemplate文件里,但是打包的时候并没有找到仓库,这是因为maven的仓库应该写在mainTemplate文件里,在launcherTemplate文件里可能会存在没有下载到的情况。

API版本问题API

这里牵扯到两个API的版本,分别是minSdkVersion和targetSdkVersion,打包的时候会报错版本问题,这里只需要在Editor–>ProjectSettings–>Player–>Other Settings里修改对应的Minimum API Level和Target API Level,修改到要求的版本或者更高的版本即可。

image.png

APK+obb分包无法运行问题

因为Google商店对上传的apk有内存限制,要求是100M以内,这里推荐使用的是APk+OBB进行分包,根据最新的要求是要求使用AAB包,这里先介绍APK+OBB的分包遇到的问题。

分开打包的方法是Editor–>ProjectSettings–>Player–>Publishing Settings里,勾选上最下面的Split Application Binary。

image.png

这个是可以代码控制的:

PlayerSettings.Android.useAPKExpansionFiles = true;

分包后如何在手机上运行呢,这里只需要安装分包后的APK,然后在手机上运行,发现第一次运行不成功,这是因为资源都在OBB中,所以无法正常运行,这里只需要吧自己的OBB改好名字放在对应的文件夹就行了。然后再运行就可以了。

文件夹地址:手机目录\Android\obb"APP的包名”
OBB文件的名字: main.安卓内部版本号.APP包名.obb (举例:main.102.com.XXX.XXX.XXX.obb)

打包AAB报错:FileNotFoundException: Temp...\launcher.aab does not exist

打包aab的方法就是打开File–>Build Settings的面板,然后勾选上Build AppBundle(Google Play)再进行打包就可以了。

image.png

这个报错网上查了一下原因,说的是因为gradle版本过高,导致unity内部逻辑出错的问题。笔者的gradle的版本确实比unity自带的版本过高,于是利用网上给的解决方案解决了。解决方法是在launcher的gradle的defaultConfig里添加下面代码,笔者不导出安卓工程于是就在launcherTemplate的defaultConfig里添加了下列的代码。

defaultConfig {

    ***

    //打包abb的话需要这个
    tasks.whenTaskAdded {
        task ->
        if (task.name.startsWith("bundle")) {
       
            def renameTaskName = "rename${task.name.capitalize()}Aab"
            def flavor = task.name.substring("bundle".length()).uncapitalize()
            tasks.create(renameTaskName, Copy) {
       
                def path = "${buildDir}/outputs/bundle/${flavor}/"
                from(path)
                include "launcher-release.aab"
                destinationDir file("${buildDir}/outputs/bundle/${flavor}/")
                rename "launcher-release.aab", "launcher.aab"
            }
     
            task.finalizedBy(renameTaskName)
        }
    }
}

AAB格式手机安装方法

首先需要把aab格式的安装包解析成apks格式的安装包,在解析的时候需要一个jar的包,这个jar包是bundletool-all-1.6.1,版本不要求一定是1.6.1,解析的方法是下面CMD的命令:

java -jar <bundletool.jar的路径> build-apks --bundle=<.aab文件的路径> --output=<输出.apks的路径> --ks=<打包.aab文件时的秘钥文件路径,如果.aab文件时没有使用秘钥则可以省去秘钥环节的配置> --ks-pass=pass:<秘钥密码> --ks-key-alias=<秘钥别名> --key-pass=pass:<秘钥别名密码> --device-spec=<要输出的目标sdkVersion的APK的json配置文件路径>

举例:

java -jar C:\Users\XX\Desktop\bundletool-all-1.0.0.jar build-apks --bundle=C:\Users\XX\Desktop\test23.aab --output=C:\Users\XX\Desktop\test23.apks --ks=G:\Client\Trunk\key\user.keystore --ks-pass=pass:abcdef --ks-key-alias=yunzhong --key-pass=pass:abcdef --device-spec=C:\Users\XX\Desktop\config.json

然后手机链接电脑,打开调试模式,接着调用CMD的安装命令:

java -jar C:\Users\XX\Desktop\bundletool-all-1.6.1.jar install-apks --apks=C:\Users\XX\Desktop\test23.apks  

安装结束后手机上就存在自己需要的安装包了。

报错Illegal usage of unity detected, shutdown unity

分包之后笔者运行发现APP直接闪退,看了半天日志最后发现了一句报错是Illegal usage of unity detected, shutdown unity。笔者使用的是unity2019.4.26f1c1(中国版,以后的中国版本后面都会有个c)。通过百度发现Unity中国版2019.4版本再分割obb编译的时候会导致这个错误,其他版本还没试过,不知道会不会有这个问题。发现只需要使用国际版本即可。

报错DSL element ‘useProguard’ is obsolete and will be removed soon. Use ‘android.enableR8’ in gradle.pro

出现这个警告是因为build.gradle里配置了 ‘useProguard’属性,而这个属性将很快被移除,使用‘android.enableR8’来代替。这里只需要在gradleTemplate.properties文件后面添加下面一句话就可以了:

android.enableR8 = true

报错自己定义的Application丢失

打包后出现自己写的Application脚本丢失,这个大部分是因为AndroidManifest没有配置自己的Application,配置方法这里就不多做介绍,网上很多介绍。笔者这里遇到的并不是因为没有配置,是因为笔者接入的SDK是继承的MultiDexApplication,这里需要注意的是如果您的 minSdkVersion 设为 21 或更高版本,系统会默认启用 MultiDex,并且您不需要 MultiDex 库。
不过,如果您的 minSdkVersion 设为 20 或更低版本,您必须使用 MultiDex 库并对应用项目进行以下修改:

android {
    defaultConfig {
        ...
        multiDexEnabled true
    }
    ...
}

dependencies {
    implementation "androidx.multidex:multidex:2.0.1"
}

此时重新编译打包后发现果然打包出多个dex文件,在安卓6.0上测试完美运行,并且用360加固以后5.0以上都能正常运行。
但是坑来了 :在5.0,5.1系统上一运行就奔溃!
后来知道在高版本系统上使用art支持多dex,而低版本dalvik默认先加载主dex,如果启动时需要的类不在主dex内就会报错ClassNotFoundException。 解压apk发现里面有上百个dex文件,一般不会拆分如此多,百度查阅后得知:
对于dex 的–multi-dex 选项设置与预编译的library工程有冲突,如果你的应用中包含引用的lirary工程,需要将预编译设置为false:
在 build.gradle中添加

dexOptions{
    preDexLibraries = false
}

SDK遇到问题

华为手机出现水滴屏无法适配的问题

笔者的项目要求手机在遇到水滴屏或者刘海屏的时候,上面显示黑条不进行渲染就可以,笔者查了一下unity的设置方法,发现只需要不勾选Editor–>ProjectSettings–>Resolution and Presentation里的Render outside safe area即可。

image.png

但是笔者发现APP在某个测试的华为手机上依旧渲染了,最后发现是接入的SDK里设置了华为手机的屏幕渲染。在华为手机Android8.0的适配方案是在AndroidManfiest里面添加下面的话即可,笔者发现接入的SDK设置了这个,于是去掉就没有问题了。

<meta-data android:name="android.notch_support" android:value="true"/> 

这里列举一下小米手机的适配方案是:

<meta-data
 android:name="notch.config"
 android:value="portrait|landscape"/>

如何修改build.gradle

关于对Android的gradle的脚本进行修改,其实上面已经介绍了。Pluging/Android文件夹下的XXXTemplate对应的其实就是导出android工程下的build.gradle,修改对应的Template就是修改对应的build.gradle。

如何添加Android需要的java脚本

首先导出一个安卓工程,然后用AndroidStudio打开导出的Android工程,然后直接在安卓工程里写对应的脚本,脚本完成后直接复制到unity工程中pluging下Android文件夹下面。这里因为每次都要复制文件,所以笔者写了一个脚本直接一键复制所有的bat脚本。脚本内容如下:

@echo off

set filePath=unityLibrary\src\main\java\com\ks
set targetFilePath=..\..\project\Assets\Plugins\Android

for /R %filePath% %%i in (*.java) do (
    xcopy /y /c /h /r %%i %targetFilePath%
    echo %%i
)
pause

后来发现每次改好脚本后,还需要找到这个bat文件执行,于是笔者简化了这个步骤,笔者添加了一个unity的编辑器脚本,用于执行这个bat文件。脚本内容如下:

public static void SyncAndroidJava2()
{
    var filepath = FileEditorTools.FormatPath(Application.dataPath + "/../../android/AndroidBDSDK_R/");
    RunBat("CopyJavaScripts.bat", "", filepath);
}

//cmd是执行的脚本的名字	args是参数,可以直接设置为“”		workingDir是执行bat文件所在文件夹路径
public static System.Diagnostics.Process CreateShellExProcess(string cmd, string args, string workingDir = "")
{
    var pStartInfo = new System.Diagnostics.ProcessStartInfo(cmd);
    pStartInfo.Arguments = args;
    pStartInfo.CreateNoWindow = false;
    pStartInfo.UseShellExecute = true;
    pStartInfo.RedirectStandardError = false;
    pStartInfo.RedirectStandardInput = false;
    pStartInfo.RedirectStandardOutput = false;
    if (!string.IsNullOrEmpty(workingDir))
        pStartInfo.WorkingDirectory = workingDir;
    return System.Diagnostics.Process.Start(pStartInfo);
}

public static void RunBat(string batfile, string args, string workingDir = "")
{
    var p = CreateShellExProcess(batfile, args, workingDir);
    p.Close();
}

在执行bat脚本的时候笔者发现,自己完全可以写一个复制用的脚本,就不需要再依靠bat脚本进行执行。脚本如下:
public static void SyncAndroidJava()
{
var filepath = FileEditorTools.FormatPath(Application.dataPath + “/../../android/AndroidBDSDK_R/unityLibrary/src/main/java/com/ks/“); //需要复制的java文件所在的文件夹
var folderpath = Application.dataPath + “/Plugins/Android/“; //复制到的文件位置
var filelist = FileEditorTools.GetallFile(filepath, “.java”);

    var curcount = 0;
    var sumcount = filelist.Count;
    
    EditorUtility.DisplayProgressBar("同步Android的Java脚本", "开始复制文字...", 0);
    foreach (var javafile in filelist)
    {
        EditorUtility.DisplayProgressBar("复制文件", javafile.FullName, (float)curcount / sumcount);
        // 判断目标目录是否存在如果不存在则新建
        try
        {
            FileEditorTools.CopyFileToFolder(javafile,folderpath);
        }
        catch (Exception e)
        {
            Debug.LogError(e);
            EditorUtility.ClearProgressBar();
            return;
        }
        Debug.Log(javafile.FullName);
        curcount++;
    }
    EditorUtility.ClearProgressBar();
}

public class FileEditorTools
{
    // 文件列表
    private static List<FileInfo> _FileList = new List<FileInfo>();
    
    #region   公有方法

    /// <summary>
    /// 获得目录下所有文件或指定文件类型文件(包含所有子文件夹)
    /// </summary>
    /// <param name="path">文件夹路径</param>
    /// <param name="extName">扩展名可以多个 例如[.mp4] [.mp3] [.wma] 等</param>
    /// <returns>List<FileInfo></returns>
    public static List<FileInfo> GetallFile(string path, string extName)
    {
        //检查目录是否存在
        if (!string.IsNullOrWhiteSpace(path))
        {
            if (Directory.Exists(path))
            {
                GetallfilesOfDir(path, extName);
            }
            else
            {
                Directory.CreateDirectory(path);
            }
        }
        else
        {
            //注意这里的EverydayLog.Write()是我自定义的日志文件,可以根据需要保留或删除
            Debug.LogError("GetAllFileOfFolder/GetallFile()/存储视频文件的路径为空,请检查!!!" );
        }
        return _FileList;
    }

    public static void CopyFileToFolder(FileInfo fileinfo,string fildername)
    {
        var destfilename = FormatPath(fildername+fileinfo.Name);
        File.Copy(fileinfo.FullName, destfilename, true);
    }
    
    public static string FormatPath(string path)
    {
        path = path.Replace("/", "\\");
        if (Application.platform == RuntimePlatform.OSXEditor)
            path = path.Replace("\\", "/");
        return path;
    }

    #endregion
    
    #region   私有方法
    /// <summary>
    /// 递归获取指定类型文件,包含子文件夹
    /// </summary>
    /// <param name="path">指定文件夹的路径</param>
    /// <param name="extName">文件拓展名</param>
    private static void GetallfilesOfDir(string path, string extName)
    {
        try
        {
            string[] dir = Directory.GetDirectories(path); //文件夹列表   
            DirectoryInfo fdir = new DirectoryInfo(path);
            FileInfo[] file = fdir.GetFiles();

            if (file.Length != 0 || dir.Length != 0) //当前目录文件或文件夹不为空                   
            {
                foreach (FileInfo f in file) //显示当前目录所有文件   
                {
                    if (extName.ToLower().IndexOf(f.Extension.ToLower()) >= 0)
                    {
                        _FileList.Add(f);
                    }
                }
                foreach (string d in dir)
                {
                    GetallfilesOfDir(d, extName);//递归   
                }
            }
        }
        catch (Exception ex)
        {
            //注意这里的EverydayLog.Write()是我自定义的日志文件,可以根据需要保留或删除
            Debug.LogError("/GetAllFileOfFolder()/GetallfilesOfDir()/获取指定路径:"+path+"   下的文件失败!!!,错误信息="+ex.Message);
        }
    }

    #endregion
    
}

设备唯一标识

这里的设备唯一标识一开始笔者用的是设备的OAID,后来发现有些设备并不能获取到设别的OAID,并且换位的手机如果打开了”关闭广告追踪“,那么获取的OAID所以这个并不能作为设别的唯一标识,网上有很多进行多数据拼接的方法,于是笔者从自己公司的SDK摘取了或者设备唯一标识的方法,这是一个比较简单的方法,就是先获取设备的androidID,如果获取不到就会自己保存一个数据到文件里,然后每次从文件里读取就行了。

private static String deviceId;
public String GetDeviceID() 
{
    Application yourApplicatoin = this;			//这里只是举个例子,这里需要大家获取一下自己的Application
    if (yourApplicatoin.getApplicationContext() == null) //这里是获取Application的实例,如果没有就可以直接返回空
        return "";
    else {
        String var1;
        if ((var1 = deviceId) != null)			//先判断deviceID是否已经赋值过了,如果已经赋值就直接返回就行了
            return var1;
        else {
            deviceId = getSPValue(yourApplicatoin, "DeviceId");	//如果没有就先从文件里获取一下
            if (!TextUtils.isEmpty(deviceId)) {					//如果获取到了就直接返回
                return deviceId;
            } else {
                deviceId = getAndroidIdAsDeviceId(yourApplicatoin);	//如果文件里没有就先尝试获取一下androidID作为设备唯一标识
                if (!TextUtils.isEmpty(deviceId)) {					//获取到了就进行保存并返回这个标识
                    saveSPValue(yourApplicatoin, "DeviceId", deviceId);	
                    return deviceId;
                } else {
                    deviceId = generateSoftDeviceId();				//如果没有获取到就通过自己的混合加密方式进行缓存
                    if (!TextUtils.isEmpty(deviceId)) {				//如果不为空就保存然后返回标识
                        saveSPValue(yourApplicatoin, "DeviceId", deviceId);
                        return deviceId;
                    } else {										//如果都没获取到就是特殊情况,直接返回
                        return deviceId;
                    }
                }
            }
        }
    }
}

保存DeviceID到文件里

通过getSharedPreferences方法将deviceid保存到文件里。

private static void saveSPValue(Context mycontext, String datakey, String datavalue) {
    mycontext.getSharedPreferences("myappsdkdeviceid", 0).edit().putString("datakey", datavalue).apply();
}

myappsdkdeviceid是文件名字,datakey是保存的关键字名字,然后datavalue是储存的值,就是我们要储存的deviceid。

从文件里获取DeviceID

private static String getSPValue(Context mycontext, String datakey) {
    return var0.getSharedPreferences("myappsdkdeviceid", 0).getString(datakey, (String)null);
}

myappsdkdeviceid是文件名字,datakey是保存的关键字名字。

获取设备的AndroidID

private static String getAndroidIdAsDeviceId(Context mycontext) {
    String andid = Settings.Secure.getString(mycontext.getContentResolver(), "android_id");	//获取设备的AndroidID
    return isLegalAndroidId(andid) ? "ANDROID_" + andid : null;				//如果符合条件就添加前缀,不符合就返回空
}

private static final Pattern ANDROID_ID_PATTERN = Pattern.compile("^[0-9a-fA-F]{16}$");

private static boolean isLegalAndroidId(String andid) {						
        return !TextUtils.isEmpty(andid) && ANDROID_ID_PATTERN.matcher(andid).find();
}

第二个函数是判断获得到的android是否不为空并且符合正则表达式的规则

自定义的设备唯一标识

private static long randomLong(long var0) {
    return Build.VERSION.SDK_INT >= 21 ? ThreadLocalRandom.current().nextLong(var0) : (long)((new Random()).nextDouble() * (double)(var0 - 1L));
}

private static String generateSoftDeviceId() {
    String arg0  = Build.SERIAL;			//首先获取序列号
    if (TextUtils.isEmpty(arg0)) {			//如果没有获取到序列化就赋值为"NA"
        arg0 = "NA";
    }

    long arg1 = 2564562216496361285L;		//设置两个随机的long型的数据
    long arg2 = 8545649582269949258L;

    arg2 = randomLong(arg2);		//获取一个随机数,获取失败就调回去重新获取
    arg1 += arg2;
    String var8 = "ANDROID_%1$s_%2$s";		//设置一下格式

    Object[] arg3 = new Object[2];
    arg3[0] = Long.toHexString(arg1);		//设置第一个参数
    try {
        arg3[1] = arg0;						//设置第二个参数
        return String.format(var8, arg3);
    } catch (Throwable var3) {
    }

    Object[] arg4;							//如果上述存在问题就根据时间设置一个随机数
    Object[] arg5 = arg4 = new Object[2];
    arg5[0] = "NA" + Long.toHexString(System.currentTimeMillis());
    arg5[1] = arg0;
    return String.format("ANDROID_%1$s_%2$s", arg4);
}

AWS(亚马逊)的CDN上传

网页上传

这种上传方式就是访问网页,然后按照需求把自己需要上传的文件上传到对应网页的进行上传。

自动上传

笔者采用的是利用python环境然后写的bat脚本进行上传。

Python环境配置

首先需要在Python虚拟环境中安装 AWS CLI

$ pip install awscli

这里介绍一个python比较好的版本管理工具,可以管理本地多版本的python。pyenv

aws版本查看

$ aws --version

image.png

更新aws

$ aws install awscli --upgrade

卸载aws

$ pip uninstall awscli  

配置AWS CLI

$ aws configure
AWS Access Key ID [None]: *******
AWS Secret Access Key [None]: *******
Default region name [None]: us-east-2
Default output format [None]: json

这里分辨需要填上对应的参数,上面的ID和Key就是自己页面申请aws给提供的。下面的json也只输出的格式,这里最关键的其实是Default region name,这里并不是随便填的,而是填上aws终端节点对应的区域代码。

image.png

这里其实会再打开终端的目录生成一个.aws文件夹,里面会有config和credentials两个文件就是我们的配置文件了。

aws与s3配合使用

想要使用aws cli上传文件需要与s3配合使用。

列举自己的库

$ aws s3 ls  

列举库中文件夹内容

$ aws s3 ls s3://my-bucket  

上传文件到s3的库

$aws s3 cp my-file s3://my-bucket/my-folder

如果每次都使用上面的命令传输文件还是比较麻烦的,所以笔者自己写了一个简单的bat脚本,可以更方便的上传文件

@echo off

set filePath=..\resource\cdnfileroot\resource\packres\default-pack\android-default
set cdnPath=s3://cyber-era-cdn/resource/packres/default-pack/android-default

call cd %filePath%

for %%i in (*.zip) do (
    call aws s3 cp %%i %cdnPath%/%%i
    echo %%i
)

pause

海外文本替换

提取Prefab中文字到表里

这里是把prefab上的文字全部提取到一个自定定义的Language表里。首先需要读取自定义的Language表里的数据,这是为了去重用的。然后加载本地所有的prefab,再遍历prefab所有的节点,然后判断是否包含Text的组件,如果包含文字就把文字记录在自己的字典中。在放进字典中是需要排重的。
加载Prefab的代码

private static void doLoadPrefab(bool clearText,bool onlyFindText = false)		//两个参数分别是是否清除Text组件和是否之查找文本,下面会有详细介绍
{
    ...

    if (string.IsNullOrEmpty(ExportExcel.excelFolder))		//接下来是查找自己的需要导入的表,不存在就创建一个新的表
    {
        excelPath = EditorUtility.OpenFilePanel("选择SVN中的ProgramLanguage表","","");
    }
    else
    {
        excelPath = ExportExcel.excelFolder + "/ProgramLanguage.xlsx";
    }
    if (!string.IsNullOrEmpty(excelPath))
    {
        ReadExcel();										//进行读取加载表格
        LoadAllPrefabText(clearText,onlyFindText);			//进行加载所有prefab的文本内容
        if (clearText)										//如果是清除文本组件的就只是需要清除字典
        {
            textDesAddDic.Clear();
        }
        else												//如果不是清除的就把读取内容写入到表格中
        {
            WroadExcel();
        }
    }
}

读取Language代码如下:

private static int exKey;
private static string exValue;
public static void ReadExcel()
{
    var attrArr = File.GetAttributes(excelPath);			//这个是获取表格的属性,因为有些表格可能是只读属性,需要修改
    File.SetAttributes(excelPath, FileAttributes.Normal);	//把表格的属性设置成普通属性,这样就一定能写入了,之所以不是去掉只读属性是因为只是单独修改可读属性不知道为何还是不能写入,就先设置为普通的属性了
    textDesDic.Clear();										//清空自己的字典
    excelFile = new FileInfo(excelPath);					//接着就是获取表格文件
    using (ExcelPackage excelPackage = new ExcelPackage(excelFile))		//下面就是循环读取表格内容然后写入到字典中
    {
        var worksheet = excelPackage.Workbook.Worksheets[1];
        for (int i = startRow; i <= worksheet.Dimension.End.Row; i++)
        {
            exKey = worksheet.Cells[i, keyColumn].GetValue<int>();
            exValue = worksheet.Cells[i, valueVolumn].GetValue<string>();
            startTextIndex = Mathf.Max(exKey, startTextIndex);
            textDesDic.Add(exKey, exValue);
        }
    }
    textDesAddDic.Clear();									//接着就是把第二个增加的字典清空,是为了记录新增的文字
    File.SetAttributes(excelPath, attrArr);					//然后就是把文件属性设置为原来的属性
}

写入Language和读取类似,只是把原来的遍历读取变成遍历新增字典,然后一行一行写入。代码如下:

public static void WroadExcel()
{
    var attrArr = File.GetAttributes(excelPath);
    File.SetAttributes(excelPath, FileAttributes.Normal);

    excelFile = new FileInfo(excelPath);
    using (ExcelPackage excelPackage = new ExcelPackage(excelFile))
    {
        var worksheet = excelPackage.Workbook.Worksheets[1];
        var curRow = worksheet.Dimension.End.Row;
        foreach (var textdespair in textDesAddDic)
        {
            worksheet.Cells[++curRow, keyColumn].Value = textdespair.Key;
            worksheet.Cells[curRow, valueVolumn].Value = textdespair.Value;
        }
        
        excelPackage.Save();								//保存表
    }
    
    File.SetAttributes(excelPath, attrArr);
    
    textDesAddDic.Clear();
}

加载prefab中的Text文本,代码如下:

static StringBuilder newTexts = new StringBuilder();
public static void LoadAllPrefabText(bool isClearText,bool onlyFindTxt = false)
{
    newTexts.Clear();							//清空字符串
    textDesAddDic.Clear();						//清空新增的字典
    var sdirs =GetAllPrefabFiles();				//获取Prefab的存在文件夹
    EditorUtility.DisplayProgressBar("Progress", "LoadPrefabTxtDes...", 0);		//打开一个进度掉,为了方便查看加载进度使用
    var asstIds = AssetDatabase.FindAssets("t:Prefab", sdirs);					//得到所有Prefab的资源
    int count = 0;								//初始化加载的进度
    for (int i = 0; i < asstIds.Length; i++)	//循环遍历一下自己加载出来的prefab
    {
        string path = AssetDatabase.GUIDToAssetPath(asstIds[i]);			//得到prefab资源的路径
        //Debug.LogError("try deal with path "+path);						
        var pfb = AssetDatabase.LoadAssetAtPath<GameObject>(path);			//根据路径加载对应的prefab
        var texts = pfb.GetComponentsInChildren<Text>(true);				//得到prefab上所有节点的Text组件
        if (texts == null || texts.Length <= 0)								//如果不存在就跳过,遍历到下一个prefab
        {
            //Debug.LogError("asset no texts: "+path);
            continue;
        }
        foreach (var item in texts)											//遍历prefab中的Text组件
        {
            textDes = item.text;											//获取组件上的文字
            if (textDes.IsNullOrWhitespace())								//如果文字为空就跳过到下一个
                continue;
            
            var langTextComp = item.gameObject.GetComponent<MutiLangText>();//获取Text文本上的脚本,自己写的替换文本的脚本
            if (langTextComp && onlyFindTxt)								//如果存在脚本并且只是查找文本,说明已经添加过就可以直接跳过了
            {
                continue;
                //Debug.LogError("has Added MutiText: "+item.name);
            }
            bool addComP = false;											//标记是否增加组件为false
            if (isClearText)												//判断是否需要清除自己的替换语言脚本
            {
                if (!textDesDic.ContainsValue(textDes))						//判断表里是否已经存在文本
                {
                    item.text = "";											//如果不存在就先清除文字
                    if (langTextComp)										//如果不存在自己的脚本就一并删除
                    {
                        //TODO  remove  comp
                        DestroyImmediate(langTextComp);
                    }
                }
                else														//如果表里存在就从字典里获取到表里的ID
                {
                    curDesId = textDesDic.Where(q => q.Value == textDes).Select(q => q.Key).ToArray()[0];
                    addComP = true;											//标记需要增加组件
                }
            }
            else															//如果不是清除文本
            {
                if (onlyFindTxt)											//如果只是查找文本
                {
                    if (!textDesDic.ContainsValue(textDes))					//如果字典中不存在就记录下来
                    {
                        newTexts.AppendLine(textDes);
                    }
                    continue;
                }
                if (!textDesDic.ContainsValue(textDes))						//如果字典中不存在,就往字典中添加,并且在新增字典中增加
                {
                    textDesDic.Add(++startTextIndex, textDes);
                    textDesAddDic.Add(startTextIndex,textDes);
                    curDesId = startTextIndex;
                }
                else														//如果存在就记录下来文本对应的ID
                {
                    curDesId = textDesDic.Where(q => q.Value == textDes).Select(q => q.Key).ToArray()[0];
                }
                
                if(!langTextComp)											//如果并没有增加切换语言脚本就标记需要增加脚本
                {
                    addComP = true;
                }
            }

            if (addComP && !langTextComp)									//如果需要增加并且组件不存在,就增加一下自己的脚本		
            {
                langTextComp = item.gameObject.AddComponent<MutiLangText>();
                langTextComp.baseText = item;
            }
            
            if (langTextComp)												//如果存在组件就更新一下ID
                langTextComp.languageID = curDesId;
        }
        PrefabUtility.SavePrefabAsset(pfb, out bool success);				//修改完毕保存prefab就可以了
        if (success)														//记录加载进度,然后更新进度条
        {
            count++;
        }
        EditorUtility.DisplayProgressBar("LoadPrefabTxtDes Progress", pfb.name, count / (float)asstIds.Length);
    }

    if (newTexts.Length > 0)												//如果有新增的文字就记录下来
    {
        Debug.LogError("write new text: "+newTexts.Length);
        File.WriteAllText(ExportExcel.excelFolder + "/newText.txt",newTexts.ToString());
    }

    EditorUtility.ClearProgressBar();										//结束之后清除加载进度条
}

获取Prefab文件路径的代码:

private static string[] GetAllPrefabFiles()
{
    string sdir = "Assets/XXX/XXX";
    List<string> sdirlist = new List<string>();
    sdirlist.Add(sdir);
    sdirlist.Add("Assets/Resources/RootPrefab");
   
    return sdirlist.ToArray();
    
}

查找代码中的中文

因为一开始没有考虑到会做海外,并且写的代码不规范,所以存在一部分中文是在代码里。这部分中文代码是不好查找的,所以写了一个小脚本,可以快速标记到中文代码的位置,这个脚本可以解决大部分,但是仍旧是存在找不到的问题的。下面来看代码:

首先会打开一个面板用来选择代码脚本所在的文件路径

[MenuItem("Tools/ReplaceText/FindScriptsLanguage")]
public static void Pack()
{
    Rect wr = new Rect(300, 400, 400, 100);
    FindChineseWindow window = (FindChineseWindow)EditorWindow.GetWindowWithRect(typeof(FindChineseWindow), wr, true, "查找项目中的中文字符");
    window.Show();
}

public class FindChineseWindow : EditorWindow
{
    private ArrayList csList = new ArrayList();
    private int eachFrameFind = 4;
    private int currentIndex = 0;
    private bool isBeginUpdate = false;
    private string outputText;
    public string filePath = "/Scripts";
    private string strForShader = "";

    //这个是需要忽略检测的文件夹
    private List<string> ingoreFileInfoDirNameList = new List<string> {"GMConsole", "LogicWorld", "NetWork", "SDK"};
    //这个是需要忽略的代码文件名
    private List<string> ingoreFileInfoNameList = new List<string> {"LocalLanguage"};
    //这个是需要忽略的代码包含的字符串
    private List<string> ingoreScriptesDesList = new List<string> {"Debug", "LogWrapper", "Tooltip", "throw new"};

    //获取需要检测的文件
    private void GetAllFile(DirectoryInfo dir)
    {
        FileInfo[] allFile = dir.GetFiles();
        foreach (FileInfo fi in allFile)
        {
            if (ingoreFileInfoDirNameList.Where(str => fi.DirectoryName.Contains(str)).Count() > 0) 
                continue;
            if (ingoreFileInfoNameList.Where(str => fi.Name.Contains(str)).Count() > 0) 
                continue;
            if (fi.FullName.IndexOf(".meta") == -1 && fi.FullName.IndexOf(".cs") != -1)
            {
                csList.Add(fi.DirectoryName + "/" + fi.Name);
            }
        }

        DirectoryInfo[] allDir = dir.GetDirectories();
        foreach (DirectoryInfo d in allDir)		//遍历子文件夹
        {
            GetAllFile(d);
        }
    }

    public void OnGUI()			//面板显示的代码
    {
        filePath = EditorGUILayout.TextField("路径:", filePath);		//输入路径

        EditorGUILayout.Space();
        EditorGUILayout.Space();

        if (GUILayout.Button("开始遍历目录"))								//显示的按钮
        {
            csList.Clear();
            DirectoryInfo d = new DirectoryInfo(Application.dataPath + filePath);	//从绝对路径读取文件
            GetAllFile(d);												//获取所有的文件
            //GetAllFile(d);
            outputText = "游戏内代码文件的数量:" + csList.Count;
            isBeginUpdate = true;
            outputText = "开始遍历项目";
        }

        GUILayout.Label(outputText, EditorStyles.boldLabel);
    }


    private bool HasChinese(string str)				//这是个判断是否是中文的方法
    {
        return Regex.IsMatch(str, @"[\u4e00-\u9fa5]");
    }

    private Regex regex = new Regex("\"[^\"]*\"");

    private void printChinese(string path)			//开始输出中文文字所在位置
    {
        if (File.Exists(path))
        {
            string[] fileContents = File.ReadAllLines(path, Encoding.Default);
            int count = fileContents.Length;
            for (int i = 0; i < count; i++)
            {
                string printStr = fileContents[i].Trim();
                if (printStr.IndexOf("//") == 0) //说明是注释
                    continue;
                if (ingoreScriptesDesList.Where(str => printStr.Contains(str)).Count() > 0) //说明是需要排除的代码
                    continue;
                MatchCollection matches = regex.Matches(printStr);
                foreach (Match match in matches)
                {
                    if (HasChinese(match.Value))
                    {
                        string[] fullPath = path.Split('/');
                        path = fullPath[fullPath.Length - 1];
                        Debug.Log("路径:" + path + " 行数:" + i + " 内容:" + printStr);
                        break;
                    }
                }
            }
        }
    }
}

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 841774407@qq.com

×

喜欢就点赞,疼爱就打赏