需求展示/参考路径
需求:查找未使用过的贴图、重复贴图,并删掉(注意需要在相应路径利用未重复的贴图)
核心参考文档:
Unity中检查重复的资源 https://www.jianshu.com/p/01a41a095a5c
重点参考文档:
C#中Directory.GetFiles() 函数的使用方法(读取目录中的文件)
https://blog.csdn.net/qq_35970739/article/details/82887314
Untiy3D 查找重复资源 https://blog.csdn.net/weixin_30530339/article/details/99903123
unity c# dictionary字典用法,dictionary嵌套用 https://blog.csdn.net/u011644138/article/details/81562606
unity 文件和文件夹的创建、删除 https://blog.csdn.net/qiao2037641855/article/details/117393110
c#读取文件并修改指定内容 https://blog.csdn.net/youshuai001/article/details/83012534
其他参考文档:
Unity-工具-检查未被使用的资源(texture、sprite、material、animation、animator)
https://blog.csdn.net/Game_Builder/article/details/95454208
Unity在编辑器中删除本地资源文件和移除组件 https://blog.csdn.net/qq_39162826/article/details/106016045
Unity用脚本自动查找重复贴图 https://blog.csdn.net/linxinfa/article/details/105256525
Unity 查找重复图片资源以及引用 工具 https://blog.csdn.net/o_ojjj/article/details/122963892
Unity清理无用资源 https://blog.csdn.net/weixin_45023328/article/details/116933055
Unity 查找资源的引用与依赖 https://blog.csdn.net/YasinXin/article/details/109150607
前置准备
目的:
检查Unity中资源是否重复,需要写一个检查工具来检查项目中是否存在重复的资源。
例如有两张贴图,明明是一张,却被复制为两份放在工程中,名字或者所在目录位置不同,这对于资源管理来说是很浪费的。
相关知识:
●Guid:
Unity会为每一个加入到Assets文件夹中的文件,创建一个同级同名的.meta文件,虽然文件类型的不同会影响这个.meta的具体内容,但它们都包含一个用来标记文件身份的File GUID**。
文件GUID提供了文件存储位置的抽象,这样一个文件GUID就对应一个具体的文件,因此我们才能随意移动这个文件而不破坏所有相关Object对这个文件的引用,而如果这个meta文件和物体不对应(比如我们SVN提交的时候没有提交修改的Meta文件),那么这个文件的引用就会丢失,造成不可预知的错误。
总结:存储了此文件和其他文件之间的引用关系
●Localid:
标识了文件内部各个文件之间的引用关系,比如一个Prefab内部的各个gameObject之间的引用关系。
●InstanceId:
Guid和Localid更像是为了文件系统的管理而设计的,那么我们在unity中实际编写代码或者引擎对object进行管理的时候每次去获取这两个大字符串显然是效率很低的,因此Unity在对象层面又通过这两个Id设计了InstanceId。简单来说,我们在工程中可以通过InstanceId来获取任何一个Object的实例。这个Id通过GetInstanceID获得,返回物体的实例ID,即使是同一个Prefab,实例化出的不同GameObject的InstanceId也是不一样的。
●MD5
unity内部通过一种算法来计算资源文件的MD5码(一个字符串),这种方法类似于哈希,将不同文件计算出不同的MD5码,但不同于哈希值的计算,我们希望MD5最好永远不要重复(哈希值计算会出现重复冲突的情况),MD5一般被认为是Unity中区分不同文件的唯一标识,那么有人肯定会疑问,万一这个值真的重复了呢。不错,确实有比较厉害的黑客可以破解Unity所使用的的计算算法,使得不同的资源文件产生相同的MD5码,不过这是非常极端的情况。我们知道热更新的时候也会用到MD5码,来标识我们需要更新哪个文件,那么在这个过程我们最好是对MD5码进行加密的,以防有恶意软件攻击我们热更新的文件。
总结:资源文件的唯一标识,即使是同一个文件(不同名字,目录)这个值也是相同的,因此可以作为我们检查资源是否重复的比较标识**
制作思路:
思路:
计算特定目录下文件的MD5码,将其存入字典中,如果字典中已经存在此key值,说明资源重复。
准备处理:
Directory.GetFiles()
大佬博客:https://blog.csdn.net/qq_35970739/article/details/82887314
官网:https://docs.microsoft.com/zh-cn/dotnet/api/system.io.directory.getfiles?view=net-6.0
描述:返回指定目录中与指定的搜索模式匹配的文件的名称(包含其路径),使用某个值确定是否要搜索子目录。
C# Directory.GetFiles()获取多个类型格式的文件 第一种方式
System.IO.Directory.GetFiles()获取多个类型格式的文件
System.IO.Directory.GetFiles(“c:\”,”(.exe|.txt)”);
第二种方式
var files = Directory.GetFiles(“C:\path”, “.“, SearchOption.AllDirectories)
.Where(s => s.EndsWith(“.mp3”) || s.EndsWith(“.jpg”));
GetFiles(String)
GetFiles(String, String)
GetFiles(String, String, EnumerationOptions)
GetFiles(String, String, SearchOption)
path | 要搜索的目录的相对或绝对路径。 此字符串不区分大小写。 |
---|---|
searchPattern | 要与 path 中的文件名匹配的搜索字符串。 此参数可以包含有效文本路径和通配符( 和 ?)的组合,但不支持正则表达式。通配符意义如下: (星号):在该位置的零个或多个字符 ?(问号):在该位置的零个或一个字符 探究:“*.mat”可搜索到”box.mat”、”box.mat1”等格式的文件,但是搜索不到文件”box.mat.meta” |
searchOption | 用于指定搜索操作是应包含所有子目录还是仅包含当前目录的枚举值之一。 SearchOption.TopDirectoryOnly 默认选项,仅包含当前目录 SearchOption.AllDirectories 包含所有子目录 |
案例:获取Assets下所有资源 Directory.GetFiles(Application.dataPath, “.“, SearchOption.AllDirectories)
Application.dataPath
public static string dataPath ;
自定义获取MD5函数
static string GetMD5Hash(string filePath)
{
MD5 md5 = new MD5CryptoServiceProvider();
//return BitConverter.ToString(md5.ComputeHash(File.ReadAllBytes(filePath))).Replace("-", "").ToLower();
return BitConverter.ToString(md5.ComputeHash(File.ReadAllBytes(filePath)));
}
概括:
① MD5CryptoServiceProvider 类(命名空间:System.Security.Cryptography) :
使用加密服务提供程序 (CSP) 提供的实现,计算输入数据的 MD5 哈希值。 此类不能被继承。
哈希函数将任意长度的二进制字符串映射到固定长度的小二进制字符串。 加密哈希函数的属性是计算上不可行的,可以查找两个哈希为同一值的不同输入:也就是说,如果相应的数据也匹配,则两组数据集的哈希应匹配。 对数据进行的少量更改会导致哈希发生较大、不可预知的更改。
②BitConverter.ToString()
public static string ToString (byte[] value);
输出形式如图
③HashAlgorithm.ComputeHash 方法
官方链接)) 描述:计算输入数据的哈希值。
public byte[] ComputeHash (byte[] buffer);
④File.ReadAllBytes()
描述:打开二进制文件,将文件内容读入一个字节数组中,然后关闭该文件。
笔记链接:https://www.yuque.com/u25921175/yrvhq9/egiq0u
⑤.Replace(“A”,”B”) 和 .ToLower()
描述:前者使用A替代字符串中的B,后者是改成小写字母。
具体实现
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.Security.Cryptography;
using System.IO;
using System;
using System.Text.RegularExpressions;
public class FindTheSame : MonoBehaviour
{
public enum fileType //查找文件格式(后缀)
{
JPG = 0,
PNG = 1,
Material = 2,
Shader = 3,
}
public enum CopyOrMove //移动到文件夹时是否保留原本数据
{
复制原数据 = 0,
}
[Header("查找文件类型(目前只开放JPG/PNG文件查找)")]
public fileType type = fileType.PNG;//默认为PNG
[Header("查找根路径(Unity资源根路径请填入:Assets/ )")]
public string searchPath = "Assets/";
[Header("查找关键字段(*零或多个字符;?零或一个字符)")]
public string keyFields = "*.*";
[Header("是否删除重复文件")]
public bool isDeleted = true;
[Header("是否在材质中替换引用重复资源图片?")]
public bool isMatReplaceTex = true;
[Header("备份重复文件>文件名:重复文件(暂存)")]
public bool isCreated = false;
[Header("备份重复数据(不修改原重复数据)")]
public CopyOrMove copyOrMoveMode = CopyOrMove.复制原数据;
private string selectedFileType = "";//选择的文件类型
private string[] matPaths;//所有的材质路径
[ContextMenu("查找重复资源并处理")]
void FindingDuplicateResources()
{
searchPath = searchPath.Trim().ToString();
keyFields = keyFields.Trim().ToString();
//满足"查找根路径:searchPath"和"查找关键字段:keyFields"去掉前后空格后非空
if (!String.IsNullOrEmpty(searchPath.Trim()) && !String.IsNullOrEmpty(keyFields.Trim()) && Directory.Exists(searchPath))
{
//--------------------------数据准备阶段------------------------
Debug.Log("满足条件,进入实现...");
selectedFileType = GetFileType(type);//获取文件类型(后缀)
//查找文件路径
string[] filePaths = Directory.GetFiles(searchPath, keyFields, SearchOption.AllDirectories);
for (int i = 0; i < filePaths.Length; i++){
filePaths[i] = filePaths[i].Replace("\\", "/");
}
// 以MD5值为key的字典
Dictionary<string, string> md5Dic = new Dictionary<string, string>();
//非重复资源的guid列表
List<string> nonRepeatGUIDs = new List<string>();
if (isMatReplaceTex)//获取所有的材质路径(相对路径)
{
matPaths = Directory.GetFiles("Assets/", "*.mat", SearchOption.AllDirectories);
for (int i = 0; i < matPaths.Length; i++)
{
matPaths[i] = matPaths[i].Replace("\\", "/");
}
}
//--------------------------数据列表准备阶段------------------------
int correctFormatNum = 0;//格式正确(总数)
int suitableFormatNum = 0;//格式正确且不重复(总数)
int repeatNum = 0;//重复(总数)
string myDumpPath = "Assets/重复文件(暂存)/"; //留存文件夹根目录
string scenePath = "Assets/Scenes/";//场景文件夹(光照贴图)--> 会出现光照贴图一致的状况
List<string> md5s = new List<string>();//md5
List<string> relativePaths = new List<string>();//相对路径
for (int i = 0; i < filePaths.Length; i++)//过滤出最终需要的重复文件格式
{
if (filePaths[i].EndsWith(".meta")){
continue;//过滤meta文件
}
if (filePaths[i].StartsWith(myDumpPath)){
continue;//过滤留存文件夹
}
if (filePaths[i].StartsWith(scenePath)){
continue;//主要过滤光照贴图(场景文件夹)
}
if (filePaths[i].EndsWith(selectedFileType))
{
string md5 = GetMD5Hash(filePaths[i]);//存储所有满足条件MD5
string relativePath = filePaths[i];//存储所有满足条件路径
correctFormatNum++;
if (!md5Dic.ContainsKey(md5))//------>md5不重复(不重复资源)
{
suitableFormatNum++;
md5Dic.Add(md5, relativePath);//绑定md5与路径(不重复路径)
//非重复资源的guid列表(每次md5不重复,则在列表添加其GUID)(不重复GUID)
nonRepeatGUIDs.Add(AssetDatabase.AssetPathToGUID(relativePath));
}else{
//此时满足 → md5Dic.ContainsKey(md5s[correctFormatNum])
//①先把引用换成不重复的那个 ②删掉、放到一个公共文件夹之中进行抉择
md5s.Add(md5);//重复路径
relativePaths.Add(relativePath);//重复GUID
Debug.LogFormat("重复资源:{0} 资源所在路径:{1}", Path.GetFileName(filePaths[i]), Path.GetDirectoryName(filePaths[i]));
}
}
}
repeatNum = correctFormatNum - suitableFormatNum;
Debug.Log("重复个数:" + repeatNum);
//--------------------------实际实现阶段------------------------
float currentCount = 0;
EditorUtility.DisplayProgressBar("进度",currentCount + "/" + repeatNum, (float)currentCount / repeatNum);//进度条
for (int i = 0; i < relativePaths.Count; i++)
{
if (isCreated)//集合到一个文件夹
{
string DeletePath = JudgDumpPaths(type);
if (!Directory.Exists(DeletePath))//判断文件夹是否存在(不存在则创建)
{
Directory.CreateDirectory(DeletePath);//创建文件夹
}
if (Directory.Exists(DeletePath))//(存在则放入数据)
{
if (relativePaths[i] != DeletePath + Path.GetFileName(relativePaths[i]))//如果转存文件夹内不存在
{
if (copyOrMoveMode == CopyOrMove.复制原数据)
{
AssetDatabase.CopyAsset(relativePaths[i], DeletePath + Path.GetFileName(relativePaths[i]));//复制文件到该路径下
}
}
}
AssetDatabase.Refresh();//刷新
}
if (isDeleted)//删除重复资源
{
if (isMatReplaceTex)//如需要替换材质中的引用图片
{
AssetDatabase.Refresh();//刷新
string repeatGUID = AssetDatabase.AssetPathToGUID(relativePaths[i]);//重复资源的GUID
string nonRepeatGUID = repeatGUID;//用于存储非重复资源GUID
for (int m = 0; m < nonRepeatGUIDs.Count; m++)
{
if (md5s[i] == GetMD5Hash(AssetDatabase.GUIDToAssetPath(nonRepeatGUIDs[m])))
{
nonRepeatGUID = nonRepeatGUIDs[m];
}
}
//Debug.Log(repeatGUID);
//Debug.Log(nonRepeatGUID);
for (int j = 0; j < matPaths.Length; j++)
{
if (File.Exists(matPaths[j]))//如果存在材质路径
{
Debug.Log("存在!");
Debug.Log(repeatGUID);
//Debug.Log(nonRepeatGUID);
string matFileRead = File.ReadAllText(matPaths[j]);
if (matFileRead != null)
{
Debug.Log("读到了");
//Debug.Log(x);
if (Regex.IsMatch(matFileRead, repeatGUID))
{
Debug.Log("通过!");
}
}
//获取资源的详细信息,然后通过正则表达式判断是否包含重复资源的guid
//if (Regex.IsMatch(File.ReadAllText(matPaths[j]), repeatGUID))
//{
// Debug.Log("替换到了!");
// string strContent = File.ReadAllText(matPaths[j]);
// strContent = Regex.Replace(strContent, repeatGUID, nonRepeatGUID);
// File.WriteAllText(matPaths[j], strContent);
//}
//AssetDatabase.DeleteAsset(relativePaths[i]);
AssetDatabase.Refresh();
}
}
}
else//不考虑材质引用,全部删除
{
AssetDatabase.DeleteAsset(relativePaths[i]);
AssetDatabase.Refresh();
}
currentCount++;
EditorUtility.DisplayProgressBar("进度", currentCount + "/" + repeatNum, (float)currentCount/repeatNum);
}
}
//关闭进度条
EditorUtility.ClearProgressBar();
}
else Debug.Log("未满足条件无法进入...");
}
/// <summary>
/// 获取需要查找重复资源的文件类型
/// </summary>
/// <returns>重复资源文件类型</returns>
/// <param name="filetype">File type.</param>
string GetFileType(fileType filetype)
{
string realFileType = ".png";
if (filetype == fileType.JPG){
realFileType = ".jpg";
}
else if (filetype == fileType.PNG){
realFileType = ".png";
}
else if (filetype == fileType.Material){
realFileType = ".mat";
}
else if (filetype == fileType.Shader){
realFileType = ".shader";
}
return realFileType;
}
/// <summary>
/// 获取文件MD5(无"-",小写字母)
/// </summary>
/// <returns>MD5 Hash.</returns>
/// <param name="filePath">File path.</param>
string GetMD5Hash(string filePath)//获取MD5
{
MD5 md5 = new MD5CryptoServiceProvider();
return BitConverter.ToString(md5.ComputeHash(File.ReadAllBytes(filePath))).Replace("-", "").ToLower();
}
/// <summary>
/// 判断转存路径
/// </summary>
/// <returns>无.</returns>
/// <param name="jude DumpPaths">Jude DumpPaths.</param>
string JudgDumpPaths(fileType fileType)
{
string dumpPath = "Assets/重复文件(暂存)/未知类型";
if(fileType == fileType.PNG)
{
dumpPath = "Assets/重复文件(暂存)/PNG/";
}
else if (fileType == fileType.JPG)
{
dumpPath = "Assets/重复文件(暂存)/JPG/";
}
else if (fileType == fileType.Shader)
{
dumpPath = "Assets/重复文件(暂存)/Shader/";
}
else if (fileType == fileType.Material)
{
dumpPath = "Assets/重复文件(暂存)/Material/";
}
return dumpPath;
}
[ContextMenu("选择查找根路径")]
void ChooseSavePath()
{
string startFolder = "Assets/";
string folderPath = EditorUtility.OpenFolderPanel("", startFolder, "");
if (searchPath.StartsWith("Assets/"))
{
searchPath = folderPath;
Debug.Log(folderPath);
}
else { searchPath = "Assets/"; }
}
}