一、工具设计背景与核心价值
1. 美术资源常见问题分类
| 问题类型 | 典型表现 | 影响程度 |
|---|---|---|
| 模型规范 | 面数超标、非法几何体 | ★★★★★ |
| 材质规范 | 非标准Shader、纹理过大 | ★★★★☆ |
| UV问题 | 重叠、拉伸、缺失 | ★★★☆☆ |
| 命名规范 | 不符合命名约定 | ★★☆☆☆ |
| 引用问题 | 材质球丢失、纹理缺失 | ★★★★☆ |
2. 自动化校验工具优势
-
一致性保障:确保所有资源符合项目技术规范
-
效率提升:将手动检查时间从小时级降至分钟级
-
问题预防:在资源导入前发现潜在问题
-
标准化流程:建立统一的资源质量门禁
-
对惹,这里有一 个游戏开发交流小组,希望大家可以点击进来一起交流一下开发经验呀
二、系统架构设计
三、核心实现代码
1. 基础校验框架
csharp
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
public enum ValidationSeverity
{
Info,
Warning,
Error,
Critical
}
public struct ValidationResult
{
public string AssetPath;
public string RuleName;
public string Message;
public ValidationSeverity Severity;
public object ContextData;
public string FixSuggestion;
}
public interface IAssetValidator
{
string ValidatorName { get; }
ValidationSeverity MinimumSeverity { get; }
bool CanValidate(string assetPath);
List<ValidationResult> Validate(string assetPath);
}
public abstract class BaseAssetValidator : IAssetValidator
{
public abstract string ValidatorName { get; }
public virtual ValidationSeverity MinimumSeverity => ValidationSeverity.Warning;
public virtual bool CanValidate(string assetPath)
{
string extension = Path.GetExtension(assetPath).ToLower();
return SupportedExtensions.Contains(extension);
}
protected abstract HashSet<string> SupportedExtensions { get; }
public abstract List<ValidationResult> Validate(string assetPath);
protected ValidationResult CreateResult(
string ruleName,
string message,
ValidationSeverity severity,
string fixSuggestion = null,
object context = null)
{
return new ValidationResult
{
RuleName = ruleName,
Message = message,
Severity = severity,
FixSuggestion = fixSuggestion,
ContextData = context
};
}
}
2. 模型校验器实现
csharp
using System.Linq;
using UnityEditor;
using UnityEngine;
public class ModelValidator : BaseAssetValidator
{
public override string ValidatorName => "模型校验器";
protected override HashSet<string> SupportedExtensions => new HashSet<string>
{
".fbx", ".obj", ".blend", ".max", ".ma", ".mb"
};
public override List<ValidationResult> Validate(string assetPath)
{
var results = new List<ValidationResult>();
var modelImporter = AssetImporter.GetAtPath(assetPath) as ModelImporter;
if (modelImporter == null)
return results;
// 检查模型设置
ValidateModelSettings(modelImporter, assetPath, results);
// 加载模型检查网格
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
if (prefab != null)
{
ValidateMeshData(prefab, assetPath, results);
ValidateRenderers(prefab, assetPath, results);
ValidateAnimations(prefab, assetPath, results);
}
return results;
}
private void ValidateModelSettings(ModelImporter importer, string path, List<ValidationResult> results)
{
// 检查缩放系数
if (importer.globalScale != 1.0f)
{
results.Add(CreateResult(
"ModelScale",
$"模型缩放系数不为1.0 (当前: {importer.globalScale})",
ValidationSeverity.Warning,
"在Model Importer中将Scale Factor设置为1.0"
));
}
// 检查导入设置
if (!importer.importBlendShapes)
{
results.Add(CreateResult(
"BlendShapes",
"模型未导入BlendShapes",
ValidationSeverity.Info
));
}
// 检查是否生成碰撞体
if (importer.addCollider)
{
results.Add(CreateResult(
"AutoCollider",
"模型自动生成碰撞体,可能影响性能",
ValidationSeverity.Warning,
"在Model Importer中禁用Add Colliders选项,手动添加合适的碰撞体"
));
}
}
private void ValidateMeshData(GameObject model, string path, List<ValidationResult> results)
{
var meshFilters = model.GetComponentsInChildren<MeshFilter>();
var skinnedMeshRenderers = model.GetComponentsInChildren<SkinnedMeshRenderer>();
int totalVertices = 0;
int totalTriangles = 0;
int meshCount = 0;
// 统计所有网格数据
foreach (var filter in meshFilters)
{
if (filter.sharedMesh != null)
{
totalVertices += filter.sharedMesh.vertexCount;
totalTriangles += filter.sharedMesh.triangles.Length / 3;
meshCount++;
ValidateSingleMesh(filter.sharedMesh, path, results);
}
}
foreach (var renderer in skinnedMeshRenderers)
{
if (renderer.sharedMesh != null)
{
totalVertices += renderer.sharedMesh.vertexCount;
totalTriangles += renderer.sharedMesh.triangles.Length / 3;
meshCount++;
ValidateSingleMesh(renderer.sharedMesh, path, results);
}
}
// 检查总面数限制
if (totalTriangles > 100000)
{
results.Add(CreateResult(
"TriangleCount",
$"模型面数过多: {totalTriangles} 三角形 (建议<10万)",
ValidationSeverity.Error,
"使用LOD或优化模型以减少面数",
totalTriangles
));
}
else if (totalTriangles > 50000)
{
results.Add(CreateResult(
"TriangleCount",
$"模型面数较高: {totalTriangles} 三角形",
ValidationSeverity.Warning,
"考虑优化模型或添加LOD"
));
}
// 检查网格数量
if (meshCount > 20)
{
results.Add(CreateResult(
"MeshCount",
$"模型包含过多子网格: {meshCount}个 (建议<20)",
ValidationSeverity.Warning,
"合并相同材质的网格以减少DrawCall"
));
}
}
private void ValidateSingleMesh(Mesh mesh, string path, List<ValidationResult> results)
{
// 检查法线
if (!mesh.HasVertexAttribute(UnityEngine.Rendering.VertexAttribute.Normal))
{
results.Add(CreateResult(
"MissingNormals",
$"网格 '{mesh.name}' 缺少法线",
ValidationSeverity.Error,
"在3D软件中生成法线后重新导入"
));
}
// 检查UV
if (!mesh.HasVertexAttribute(UnityEngine.Rendering.VertexAttribute.TexCoord0))
{
results.Add(CreateResult(
"MissingUVs",
$"网格 '{mesh.name}' 缺少主UV通道",
ValidationSeverity.Error,
"在3D软件中展开UV后重新导入"
));
}
// 检查UV范围
if (mesh.HasVertexAttribute(UnityEngine.Rendering.VertexAttribute.TexCoord0))
{
var uvBounds = GetUVBounds(mesh);
if (uvBounds.min.x < 0 || uvBounds.min.y < 0 ||
uvBounds.max.x > 1 || uvBounds.max.y > 1)
{
results.Add(CreateResult(
"UVOutOfBounds",
$"网格 '{mesh.name}' UV超出[0,1]范围",
ValidationSeverity.Warning,
"将UV调整到[0,1]范围内以避免纹理重复问题"
));
}
}
// 检查三角形有效性
ValidateMeshTriangles(mesh, path, results);
}
private Bounds GetUVBounds(Mesh mesh)
{
var uvs = mesh.uv;
Vector2 min = new Vector2(float.MaxValue, float.MaxValue);
Vector2 max = new Vector2(float.MinValue, float.MinValue);
foreach (var uv in uvs)
{
min = Vector2.Min(min, uv);
max = Vector2.Max(max, uv);
}
return new Bounds((min + max) * 0.5f, max - min);
}
private void ValidateMeshTriangles(Mesh mesh, string path, List<ValidationResult> results)
{
int[] triangles = mesh.triangles;
Vector3[] vertices = mesh.vertices;
for (int i = 0; i < triangles.Length; i += 3)
{
int i1 = triangles[i];
int i2 = triangles[i + 1];
int i3 = triangles[i + 2];
// 检查重复顶点索引
if (i1 == i2 || i1 == i3 || i2 == i3)
{
results.Add(CreateResult(
"DegenerateTriangle",
$"网格 '{mesh.name}' 包含退化三角形",
ValidationSeverity.Error,
"修复网格中的退化三角形"
));
break;
}
// 检查三角形面积
Vector3 v1 = vertices[i1];
Vector3 v2 = vertices[i2];
Vector3 v3 = vertices[i3];
float area = Vector3.Cross(v2 - v1, v3 - v1).magnitude * 0.5f;
if (area < 0.0001f)
{
results.Add(CreateResult(
"ZeroAreaTriangle",
$"网格 '{mesh.name}' 包含零面积三角形",
ValidationSeverity.Warning,
"检查并修复网格中的微小三角形"
));
break;
}
}
}
private void ValidateRenderers(GameObject model, string path, List<ValidationResult> results)
{
var renderers = model.GetComponentsInChildren<Renderer>();
foreach (var renderer in renderers)
{
// 检查材质数量
if (renderer.sharedMaterials.Length > 1)
{
results.Add(CreateResult(
"MultipleMaterials",
$"渲染器 '{renderer.name}' 使用多个材质({renderer.sharedMaterials.Length})",
ValidationSeverity.Info,
"考虑合并材质以提高性能"
));
}
// 检查材质球引用
foreach (var material in renderer.sharedMaterials)
{
if (material == null)
{
results.Add(CreateResult(
"MissingMaterial",
$"渲染器 '{renderer.name}' 材质球丢失",
ValidationSeverity.Error,
"重新分配材质球"
));
}
}
}
}
private void ValidateAnimations(GameObject model, string path, List<ValidationResult> results)
{
var animator = model.GetComponent<Animator>();
var animation = model.GetComponent<Animation>();
if (animator != null)
{
// 检查Animator Controller
if (animator.runtimeAnimatorController == null)
{
results.Add(CreateResult(
"MissingAnimatorController",
"Animator缺少Controller",
ValidationSeverity.Warning
));
}
// 检查Avatar
if (animator.avatar == null)
{
results.Add(CreateResult(
"MissingAvatar",
"Animator缺少Avatar",
ValidationSeverity.Error,
"导入模型时确保已生成Avatar"
));
}
}
}
}
3. 材质校验器实现
csharp
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
public class MaterialValidator : BaseAssetValidator
{
// 允许的Shader白名单
private static readonly HashSet<string> AllowedShaders = new HashSet<string>
{
"Universal Render Pipeline/Lit",
"Universal Render Pipeline/Simple Lit",
"Universal Render Pipeline/Unlit",
"HDRP/Lit",
"HDRP/Unlit",
"Standard",
"Standard (Specular setup)"
};
public override string ValidatorName => "材质校验器";
protected override HashSet<string> SupportedExtensions => new HashSet<string>
{
".mat", ".physicmaterial", ".physicsmaterial2d"
};
public override List<ValidationResult> Validate(string assetPath)
{
var results = new List<ValidationResult>();
var material = AssetDatabase.LoadAssetAtPath<Material>(assetPath);
if (material == null)
return results;
ValidateShader(material, assetPath, results);
ValidateTextures(material, assetPath, results);
ValidateMaterialProperties(material, assetPath, results);
return results;
}
private void ValidateShader(Material material, string path, List<ValidationResult> results)
{
string shaderName = material.shader.name;
// 检查是否使用非标准Shader
if (!AllowedShaders.Contains(shaderName))
{
results.Add(CreateResult(
"NonStandardShader",
$"材质使用非标准Shader: {shaderName}",
ValidationSeverity.Error,
$"更换为允许的Shader: {string.Join(", ", AllowedShaders)}",
shaderName
));
}
// 检查Shader关键字
if (material.IsKeywordEnabled("_SPECULARHIGHLIGHTS_OFF"))
{
results.Add(CreateResult(
"DisabledSpecular",
"材质禁用了高光反射",
ValidationSeverity.Info
));
}
}
private void ValidateTextures(Material material, string path, List<ValidationResult> results)
{
int textureCount = 0;
int maxTextureSize = 0;
long totalTextureSize = 0;
// 获取材质所有纹理属性
int propertyCount = ShaderUtil.GetPropertyCount(material.shader);
for (int i = 0; i < propertyCount; i++)
{
if (ShaderUtil.GetPropertyType(material.shader, i) == ShaderUtil.ShaderPropertyType.TexEnv)
{
string propertyName = ShaderUtil.GetPropertyName(material.shader, i);
Texture texture = material.GetTexture(propertyName);
if (texture != null)
{
textureCount++;
ValidateSingleTexture(texture, propertyName, path, results,
ref maxTextureSize, ref totalTextureSize);
}
}
}
// 检查纹理总数
if (textureCount > 8)
{
results.Add(CreateResult(
"TooManyTextures",
$"材质使用过多纹理: {textureCount}张 (建议≤8)",
ValidationSeverity.Warning,
"考虑合并纹理或使用纹理图集"
));
}
// 检查纹理总大小
if (totalTextureSize > 50 * 1024 * 1024) // 50MB
{
results.Add(CreateResult(
"LargeTextures",
$"材质纹理总大小过大: {totalTextureSize / (1024*1024)}MB",
ValidationSeverity.Warning,
"压缩纹理或降低分辨率"
));
}
}
private void ValidateSingleTexture(Texture texture, string propertyName,
string path, List<ValidationResult> results,
ref int maxTextureSize, ref long totalTextureSize)
{
string texturePath = AssetDatabase.GetAssetPath(texture);
if (!string.IsNullOrEmpty(texturePath))
{
var importer = AssetImporter.GetAtPath(texturePath) as TextureImporter;
if (importer != null)
{
// 检查纹理尺寸是否为2的幂
bool isPowerOfTwo = Mathf.IsPowerOfTwo(texture.width) &&
Mathf.IsPowerOfTwo(texture.height);
if (!isPowerOfTwo)
{
results.Add(CreateResult(
"NonPowerOfTwoTexture",
$"纹理 '{texture.name}' 尺寸非2的幂: {texture.width}x{texture.height}",
ValidationSeverity.Warning,
"调整纹理尺寸为2的幂以提高兼容性"
));
}
// 检查纹理尺寸限制
int maxDimension = Mathf.Max(texture.width, texture.height);
maxTextureSize = Mathf.Max(maxTextureSize, maxDimension);
if (maxDimension > 4096)
{
results.Add(CreateResult(
"OversizedTexture",
$"纹理 '{texture.name}' 尺寸过大: {texture.width}x{texture.height}",
ValidationSeverity.Error,
"将纹理尺寸缩小到4096x4096以内"
));
}
else if (maxDimension > 2048)
{
results.Add(CreateResult(
"LargeTexture",
$"纹理 '{texture.name}' 尺寸较大: {texture.width}x{texture.height}",
ValidationSeverity.Warning,
"考虑使用2048x2048或更小的纹理"
));
}
// 检查纹理格式
ValidateTextureFormat(importer, texture, path, results);
// 估算纹理内存大小
totalTextureSize += EstimateTextureSize(texture, importer);
}
}
}
private void ValidateTextureFormat(TextureImporter importer, Texture texture,
string path, List<ValidationResult> results)
{
// 检查压缩格式
bool isCompressed = false;
#if UNITY_ANDROID
isCompressed = importer.GetPlatformTextureSettings("Android").format ==
TextureImporterFormat.ASTC_6x6;
#elif UNITY_IOS
isCompressed = importer.GetPlatformTextureSettings("iPhone").format ==
TextureImporterFormat.ASTC_6x6;
#else
isCompressed = importer.textureCompression != TextureImporterCompression.Uncompressed;
#endif
if (!isCompressed)
{
results.Add(CreateResult(
"UncompressedTexture",
$"纹理 '{texture.name}' 未压缩",
ValidationSeverity.Warning,
"启用纹理压缩以减少内存占用"
));
}
// 检查Mipmap
if (!importer.mipmapEnabled)
{
results.Add(CreateResult(
"MissingMipmaps",
$"纹理 '{texture.name}' 未生成Mipmaps",
ValidationSeverity.Info,
"为3D模型纹理启用Mipmaps以改善渲染质量"
));
}
}
private long EstimateTextureSize(Texture texture, TextureImporter importer)
{
// 简化的纹理大小估算
int mipmapFactor = importer.mipmapEnabled ? 4 : 1;
int bpp = 4; // 假设RGBA32
return (long)texture.width * texture.height * bpp * mipmapFactor;
}
private void ValidateMaterialProperties(Material material, string path, List<ValidationResult> results)
{
// 检查透明材质设置
if (material.renderQueue >= 3000)
{
results.Add(CreateResult(
"TransparentMaterial",
"材质使用透明渲染队列",
ValidationSeverity.Info,
"确保透明材质正确排序以避免渲染问题"
));
}
// 检查双面渲染
if (material.doubleSidedGI)
{
results.Add(CreateResult(
"DoubleSidedGI",
"材质启用了双面全局光照",
ValidationSeverity.Info,
"双面GI会增加光照计算开销"
));
}
}
}
4. 纹理校验器实现
csharp
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
public class TextureValidator : BaseAssetValidator
{
public override string ValidatorName => "纹理校验器";
protected override HashSet<string> SupportedExtensions => new HashSet<string>
{
".png", ".jpg", ".jpeg", ".tga", ".tif", ".tiff", ".bmp", ".psd", ".exr"
};
public override List<ValidationResult> Validate(string assetPath)
{
var results = new List<ValidationResult>();
var importer = AssetImporter.GetAtPath(assetPath) as TextureImporter;
if (importer == null)
return results;
ValidateImportSettings(importer, assetPath, results);
ValidateTextureType(importer, assetPath, results);
ValidateReadability(importer, assetPath, results);
return results;
}
private void ValidateImportSettings(TextureImporter importer, string path, List<ValidationResult> results)
{
// 检查最大尺寸设置
int maxSize = importer.maxTextureSize;
if (maxSize > 4096)
{
results.Add(CreateResult(
"MaxSizeTooLarge",
$"纹理最大尺寸设置过大: {maxSize}",
ValidationSeverity.Warning,
"根据实际使用情况调整最大尺寸"
));
}
// 检查非2的幂处理策略
if (importer.npotScale != TextureImporterNPOTScale.ToNearest)
{
results.Add(CreateResult(
"NPOTHandling",
$"非2的幂纹理处理方式为: {importer.npotScale}",
ValidationSeverity.Info,
"建议使用ToNearest确保纹理尺寸为2的幂"
));
}
// 检查Alpha通道处理
bool hasAlpha = importer.DoesSourceTextureHaveAlpha();
if (hasAlpha && importer.alphaSource == TextureImporterAlphaSource.None)
{
results.Add(CreateResult(
"AlphaChannelIgnored",
"纹理包含Alpha通道但导入设置忽略Alpha",
ValidationSeverity.Warning,
"如果不需要Alpha通道,建议在源文件中移除"
));
}
}
private void ValidateTextureType(TextureImporter importer, string path, List<ValidationResult> results)
{
// 检查纹理类型设置
switch (importer.textureType)
{
case TextureImporterType.Default:
// 默认纹理检查
if (importer.textureShape != TextureImporterShape.Texture2D)
{
results.Add(CreateResult(
"Non2DTexture",
"纹理不是2D类型",
ValidationSeverity.Warning,
"2D纹理通常性能更好"
));
}
break;
case TextureImporterType.NormalMap:
// 法线贴图检查
if (importer.convertToNormalMap)
{
results.Add(CreateResult(
"AutoNormalMap",
"纹理设置为自动转换为法线贴图",
ValidationSeverity.Info,
"确保源图像适合做法线贴图"
));
}
break;
case TextureImporterType.Sprite:
// Sprite检查
var spriteSettings = importer.spritePixelsPerUnit;
if (spriteSettings != 100)
{
results.Add(CreateResult(
"SpritePPU",
$"Sprite Pixels Per Unit不是标准值: {spriteSettings}",
ValidationSeverity.Info,
"建议使用100作为标准值"
));
}
break;
}
}
private void ValidateReadability(TextureImporter importer, string path, List<ValidationResult> results)
{
// 检查是否启用Read/Write
if (importer.isReadable)
{
results.Add(CreateResult(
"TextureReadable",
"纹理启用了Read/Write选项",
ValidationSeverity.Warning,
"除非需要CPU访问纹理数据,否则禁用此选项以减少内存占用"
));
}
// 检查Streaming Mipmaps
if (importer.streamingMipmaps)
{
results.Add(CreateResult(
"StreamingMipmaps",
"纹理启用了Streaming Mipmaps",
ValidationSeverity.Info,
"适合大型纹理,但可能增加加载时间"
));
}
}
}
5. 编辑器界面实现
csharp
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
public class AssetValidationWindow : EditorWindow
{
[MenuItem("Tools/美术资源校验工具")]
public static void ShowWindow()
{
GetWindow<AssetValidationWindow>("资源校验工具");
}
// 校验器列表
private List<IAssetValidator> validators = new List<IAssetValidator>
{
new ModelValidator(),
new MaterialValidator(),
new TextureValidator()
};
// UI状态
private string targetFolder = "Assets/";
private bool isScanning = false;
private Vector2 scrollPosition;
private ValidationSeverity minimumSeverity = ValidationSeverity.Warning;
// 结果存储
private List<ValidationResult> allResults = new List<ValidationResult>();
private Dictionary<string, List<ValidationResult>> resultsByAsset =
new Dictionary<string, List<ValidationResult>>();
// 树状视图
private TreeViewState treeViewState;
private ValidationTreeView validationTreeView;
private SearchField searchField;
private string searchString = "";
private void OnEnable()
{
// 初始化树状视图
treeViewState = new TreeViewState();
validationTreeView = new ValidationTreeView(treeViewState);
searchField = new SearchField();
}
private void OnGUI()
{
DrawToolbar();
DrawFolderSelection();
DrawSeverityFilter();
EditorGUILayout.Space();
if (isScanning)
{
EditorGUILayout.HelpBox("正在扫描资源...", MessageType.Info);
return;
}
if (allResults.Count == 0)
{
EditorGUILayout.HelpBox("选择文件夹并点击扫描按钮开始校验", MessageType.Info);
}
else
{
DrawResultsSummary();
DrawResultsTree();
DrawActionButtons();
}
}
private void DrawToolbar()
{
GUILayout.BeginHorizontal(EditorStyles.toolbar);
if (GUILayout.Button("扫描选中文件夹", EditorStyles.toolbarButton, GUILayout.Width(120)))
{
StartValidation();
}
if (GUILayout.Button("扫描整个项目", EditorStyles.toolbarButton, GUILayout.Width(120)))
{
targetFolder = "Assets";
StartValidation();
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("导出报告", EditorStyles.toolbarButton, GUILayout.Width(80)))
{
ExportReport();
}
if (GUILayout.Button("自动修复", EditorStyles.toolbarButton, GUILayout.Width(80)))
{
AutoFixIssues();
}
GUILayout.EndHorizontal();
}
private void DrawFolderSelection()
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("目标文件夹:", GUILayout.Width(80));
EditorGUILayout.LabelField(targetFolder, EditorStyles.textField);
if (GUILayout.Button("选择...", GUILayout.Width(60)))
{
string newFolder = EditorUtility.OpenFolderPanel("选择文件夹", targetFolder, "");
if (!string.IsNullOrEmpty(newFolder))
{
if (newFolder.StartsWith(Application.dataPath))
{
targetFolder = "Assets" + newFolder.Substring(Application.dataPath.Length);
}
else
{
EditorUtility.DisplayDialog("错误", "请选择项目内的文件夹", "确定");
}
}
}
EditorGUILayout.EndHorizontal();
}
private void DrawSeverityFilter()
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("最低严重等级:", GUILayout.Width(100));
minimumSeverity = (ValidationSeverity)EditorGUILayout.EnumPopup(minimumSeverity, GUILayout.Width(100));
EditorGUILayout.EndHorizontal();
}
private void DrawResultsSummary()
{
int errorCount = allResults.Count(r => r.Severity == ValidationSeverity.Error ||
r.Severity == ValidationSeverity.Critical);
int warningCount = allResults.Count(r => r.Severity == ValidationSeverity.Warning);
int infoCount = allResults.Count(r => r.Severity == ValidationSeverity.Info);
GUILayout.BeginHorizontal();
EditorGUILayout.HelpBox(
$"扫描完成! 发现 {allResults.Count} 个问题 " +
$"(错误: {errorCount}, 警告: {warningCount}, 信息: {infoCount})",
errorCount > 0 ? MessageType.Error :
warningCount > 0 ? MessageType.Warning : MessageType.Info
);
GUILayout.EndHorizontal();
}
private void DrawResultsTree()
{
// 搜索框
Rect searchRect = GUILayoutUtility.GetRect(1, 1, 20, 20);
searchString = searchField.OnGUI(searchRect, searchString);
// 树状视图
Rect treeRect = GUILayoutUtility.GetRect(1, 1, 200, 1000);
validationTreeView.searchString = searchString;
validationTreeView.OnGUI(treeRect);
}
private void DrawActionButtons()
{
GUILayout.BeginHorizontal();
if (GUILayout.Button("选择所有错误", GUILayout.Width(120)))
{
SelectAllAssetsWithSeverity(ValidationSeverity.Error);
}
if (GUILayout.Button("选择所有警告", GUILayout.Width(120)))
{
SelectAllAssetsWithSeverity(ValidationSeverity.Warning);
}
GUILayout.EndHorizontal();
}
private void StartValidation()
{
isScanning = true;
// 清空之前的结果
allResults.Clear();
resultsByAsset.Clear();
// 开始异步扫描
EditorApplication.delayCall += () =>
{
ScanAssetsRecursive(targetFolder);
BuildTreeView();
isScanning = false;
Repaint();
};
}
private void ScanAssetsRecursive(string folderPath)
{
// 获取文件夹内所有资源
string[] assetPaths = Directory.GetFiles(folderPath, "*.*", SearchOption.AllDirectories)
.Where(p => !p.EndsWith(".meta"))
.ToArray();
int total = assetPaths.Length;
int current = 0;
foreach (var assetPath in assetPaths)
{
current++;
// 显示进度
if (EditorUtility.DisplayCancelableProgressBar(
"扫描资源",
$"正在扫描: {Path.GetFileName(assetPath)}",
(float)current / total))
{
break;
}
ValidateAsset(assetPath);
}
EditorUtility.ClearProgressBar();
}
private void ValidateAsset(string assetPath)
{
var assetResults = new List<ValidationResult>();
foreach (var validator in validators)
{
if (validator.CanValidate(assetPath) && validator.MinimumSeverity <= minimumSeverity)
{
var results = validator.Validate(assetPath);
if (results != null)
{
// 过滤低于最小严重等级的结果
var filteredResults = results.Where(r => r.Severity >= minimumSeverity).ToList();
assetResults.AddRange(filteredResults);
}
}
}
if (assetResults.Count > 0)
{
allResults.AddRange(assetResults);
resultsByAsset[assetPath] = assetResults;
}
}
private void BuildTreeView()
{
// 构建树状视图的数据
var treeData = new List<ValidationTreeElement>();
int id = 0;
// 按严重等级分组
var groupedBySeverity = allResults
.GroupBy(r => r.Severity)
.OrderByDescending(g => g.Key);
foreach (var severityGroup in groupedBySeverity)
{
string severityName = GetSeverityDisplayName(severityGroup.Key);
Color severityColor = GetSeverityColor(severityGroup.Key);
var severityItem = new ValidationTreeElement
{
id = id++,
name = $"{severityName} ({severityGroup.Count()})",
depth = 0,
severity = severityGroup.Key,
assetPath = null
};
treeData.Add(severityItem);
// 按资源分组
var groupedByAsset = severityGroup.GroupBy(r => r.AssetPath);
foreach (var assetGroup in groupedByAsset)
{
var assetItem = new ValidationTreeElement
{
id = id++,
name = Path.GetFileName(assetGroup.Key),
depth = 1,
severity = severityGroup.Key,
assetPath = assetGroup.Key
};
treeData.Add(assetItem);
// 添加具体问题
foreach (var result in assetGroup)
{
var problemItem = new ValidationTreeElement
{
id = id++,
name = $"{result.RuleName}: {result.Message}",
depth = 2,
severity = result.Severity,
assetPath = assetGroup.Key,
validationResult = result
};
treeData.Add(problemItem);
}
}
}
validationTreeView.SetData(treeData);
}
private string GetSeverityDisplayName(ValidationSeverity severity)
{
return severity switch
{
ValidationSeverity.Critical => "严重错误",
ValidationSeverity.Error => "错误",
ValidationSeverity.Warning => "警告",
ValidationSeverity.Info => "信息",
_ => "未知"
};
}
private Color GetSeverityColor(ValidationSeverity severity)
{
return severity switch
{
ValidationSeverity.Critical => Color.red,
ValidationSeverity.Error => new Color(1, 0.5f, 0), // 橙色
ValidationSeverity.Warning => Color.yellow,
ValidationSeverity.Info => Color.white,
_ => Color.gray
};
}
private void SelectAllAssetsWithSeverity(ValidationSeverity severity)
{
var assets = allResults
.Where(r => r.Severity == severity)
.Select(r => r.AssetPath)
.Distinct()
.ToArray();
var assetObjects = assets
.Select(p => AssetDatabase.LoadAssetAtPath<Object>(p))
.Where(o => o != null)
.ToArray();
Selection.objects = assetObjects;
}
private void ExportReport()
{
string reportPath = EditorUtility.SaveFilePanel(
"导出报告",
Application.dataPath,
$"AssetValidationReport_{System.DateTime.Now:yyyyMMdd_HHmmss}",
"csv");
if (string.IsNullOrEmpty(reportPath))
return;
using (var writer = new StreamWriter(reportPath))
{
// 写入CSV标题
writer.WriteLine("资源路径,严重等级,规则名称,问题描述,修复建议");
// 写入数据
foreach (var result in allResults.OrderByDescending(r => r.Severity))
{
writer.WriteLine($"\"{result.AssetPath}\"," +
$"\"{result.Severity}\"," +
$"\"{result.RuleName}\"," +
$"\"{result.Message}\"," +
$"\"{result.FixSuggestion}\"");
}
}
EditorUtility.RevealInFinder(reportPath);
}
private void AutoFixIssues()
{
// 实现自动修复逻辑
int fixedCount = 0;
foreach (var assetPath in resultsByAsset.Keys)
{
var results = resultsByAsset[assetPath];
foreach (var result in results)
{
if (TryAutoFix(result, assetPath))
{
fixedCount++;
}
}
}
EditorUtility.DisplayDialog("自动修复完成",
$"已尝试修复 {fixedCount} 个问题", "确定");
// 重新扫描验证修复结果
StartValidation();
}
private bool TryAutoFix(ValidationResult result, string assetPath)
{
// 根据规则名称实现具体的自动修复逻辑
switch (result.RuleName)
{
case "ModelScale":
var modelImporter = AssetImporter.GetAtPath(assetPath) as ModelImporter;
if (modelImporter != null)
{
modelImporter.globalScale = 1.0f;
modelImporter.SaveAndReimport();
return true;
}
break;
case "TextureReadable":
var textureImporter = AssetImporter.GetAtPath(assetPath) as TextureImporter;
if (textureImporter != null && textureImporter.isReadable)
{
textureImporter.isReadable = false;
textureImporter.SaveAndReimport();
return true;
}
break;
// 添加更多自动修复规则...
}
return false;
}
}
// 树状视图元素
public class ValidationTreeElement
{
public int id;
public string name;
public int depth;
public ValidationSeverity severity;
public string assetPath;
public ValidationResult validationResult;
}
// 树状视图实现
public class ValidationTreeView : TreeView
{
private List<ValidationTreeElement> treeElements = new List<ValidationTreeElement>();
public ValidationTreeView(TreeViewState state) : base(state)
{
showAlternatingRowBackgrounds = true;
showBorder = true;
}
public void SetData(List<ValidationTreeElement> elements)
{
treeElements = elements;
Reload();
}
protected override TreeViewItem BuildRoot()
{
var root = new TreeViewItem { id = -1, depth = -1, displayName = "Root" };
if (treeElements.Count == 0)
{
var item = new TreeViewItem { id = 0, depth = 0, displayName = "无校验结果" };
root.AddChild(item);
}
else
{
foreach (var element in treeElements)
{
var item = new TreeViewItem
{
id = element.id,
depth = element.depth,
displayName = element.name
};
root.AddChild(item);
}
}
SetupDepthsFromParentsAndChildren(root);
return root;
}
protected override void RowGUI(RowGUIArgs args)
{
var element = treeElements.FirstOrDefault(e => e.id == args.item.id);
if (element != null)
{
// 根据严重等级设置颜色
Color color = GetSeverityColor(element.severity);
using (new GUILayout.HorizontalScope())
{
// 绘制严重等级图标
Rect iconRect = args.rowRect;
iconRect.width = 20;
if (Event.current.type == EventType.Repaint)
{
EditorGUI.DrawRect(iconRect, color);
}
// 绘制文本
Rect labelRect = args.rowRect;
labelRect.x += 25;
labelRect.width -= 25;
GUI.Label(labelRect, args.item.displayName);
// 如果是具体问题,显示修复按钮
if (element.validationResult != null && !string.IsNullOrEmpty(element.validationResult.FixSuggestion))
{
Rect buttonRect = labelRect;
buttonRect.x = buttonRect.xMax - 80;
buttonRect.width = 80;
if (GUI.Button(buttonRect, "修复"))
{
ShowFixDialog(element.validationResult, element.assetPath);
}
}
}
}
else
{
base.RowGUI(args);
}
}
private Color GetSeverityColor(ValidationSeverity severity)
{
return severity switch
{
ValidationSeverity.Critical => Color.red,
ValidationSeverity.Error => new Color(1, 0.5f, 0),
ValidationSeverity.Warning => Color.yellow,
ValidationSeverity.Info => Color.cyan,
_ => Color.gray
};
}
protected override void DoubleClickedItem(int id)
{
var element = treeElements.FirstOrDefault(e => e.id == id);
if (element != null && !string.IsNullOrEmpty(element.assetPath))
{
// 在Project窗口中选择资源
var asset = AssetDatabase.LoadAssetAtPath<Object>(element.assetPath);
if (asset != null)
{
Selection.activeObject = asset;
EditorGUIUtility.PingObject(asset);
}
}
}
private void ShowFixDialog(ValidationResult result, string assetPath)
{
if (EditorUtility.DisplayDialog("修复问题",
$"问题: {result.Message}\n\n建议修复方式: {result.FixSuggestion}",
"自动修复", "手动修复"))
{
// 尝试自动修复
if (!TryAutoFix(result, assetPath))
{
EditorUtility.DisplayDialog("无法自动修复",
"此问题无法自动修复,请手动处理", "确定");
}
}
else
{
// 选中资源让用户手动修复
var asset = AssetDatabase.LoadAssetAtPath<Object>(assetPath);
if (asset != null)
{
Selection.activeObject = asset;
EditorGUIUtility.PingObject(asset);
}
}
}
private bool TryAutoFix(ValidationResult result, string assetPath)
{
// 这里可以调用主窗口的自动修复逻辑
return false;
}
}
四、高级功能扩展
1. 自定义规则系统
csharp
using System;
using UnityEngine;
[CreateAssetMenu(fileName = "ValidationRule", menuName = "Tools/校验规则")]
public class ValidationRule : ScriptableObject
{
public string RuleId;
public string DisplayName;
public ValidationSeverity Severity;
[TextArea(3, 10)]
public string Description;
[TextArea(2, 5)]
public string FixSuggestion;
// 条件判断
public RuleCondition[] Conditions;
// 适用于的资源类型
public AssetType TargetAssetType;
public enum AssetType
{
Model,
Material,
Texture,
Animation,
Prefab,
Any
}
[Serializable]
public struct RuleCondition
{
public ConditionType Type;
public string PropertyPath;
public CompareOperator Operator;
public string Value;
public enum ConditionType
{
PropertyValue,
FileSize,
Dimension,
Count
}
public enum CompareOperator
{
Equal,
NotEqual,
GreaterThan,
LessThan,
Contains,
StartsWith,
EndsWith
}
}
}
2. 批量处理工具
csharp
using System.Threading.Tasks;
using UnityEditor;
public class BatchAssetProcessor
{
public async Task ProcessFolderAsync(string folderPath, System.Action<string> onProgress)
{
var assetPaths = AssetDatabase.FindAssets("", new[] { folderPath })
.Select(AssetDatabase.GUIDToAssetPath)
.Where(p => !p.EndsWith(".meta"))
.ToArray();
int total = assetPaths.Length;
int processed = 0;
foreach (var assetPath in assetPaths)
{
processed++;
onProgress?.Invoke($"处理中 ({processed}/{total}): {assetPath}");
await ProcessAssetAsync(assetPath);
if (processed % 10 == 0)
{
// 每处理10个资源保存一次,避免数据丢失
AssetDatabase.SaveAssets();
}
}
onProgress?.Invoke("批量处理完成");
}
private async Task ProcessAssetAsync(string assetPath)
{
// 模拟异步处理
await Task.Delay(10);
// 实际处理逻辑
// ...
}
}
3. CI/CD集成
csharp
using UnityEditor;
using UnityEditor.Build.Reporting;
public class ValidationBuildPreprocess : IPreprocessBuildWithReport
{
public int callbackOrder => 0;
public void OnPreprocessBuild(BuildReport report)
{
var validator = new AssetValidationSystem();
var results = validator.ValidateAllAssets();
int errorCount = results.Count(r =>
r.Severity == ValidationSeverity.Error ||
r.Severity == ValidationSeverity.Critical);
if (errorCount > 0)
{
string errorMessage = $"发现 {errorCount} 个资源错误,构建中止。\n";
errorMessage += string.Join("\n",
results.Where(r => r.Severity >= ValidationSeverity.Error)
.Select(r => $"- {r.AssetPath}: {r.Message}"));
throw new BuildFailedException(errorMessage);
}
int warningCount = results.Count(r => r.Severity == ValidationSeverity.Warning);
if (warningCount > 0)
{
Debug.LogWarning($"构建前发现 {warningCount} 个资源警告");
}
}
}
五、最佳实践建议
1. 项目配置策略
yaml
# Assets/Editor/AssetValidationConfig.yaml
validation_rules:
models:
max_triangles: 100000
max_meshes: 20
require_normals: true
require_uv1: false
materials:
allowed_shaders:
- "Universal Render Pipeline/Lit"
- "Universal Render Pipeline/Simple Lit"
max_textures: 8
max_texture_size: 4096
textures:
require_power_of_two: true
max_dimension: 4096
require_compression: true
file_naming:
pattern: "^[A-Z][a-zA-Z0-9_]*$"
prefix_mapping:
material: "M_"
texture: "T_"
model: "MDL_"
2. 性能优化建议
| 场景 | 优化策略 | 效果 |
|---|---|---|
| 大型项目 | 增量校验,只检查修改的资源 | 扫描时间减少80% |
| 频繁校验 | 缓存校验结果 | 重复校验时间减少95% |
| 批量处理 | 并行校验 | 处理速度提升3-5倍 |
六、完整项目结构
text
Assets/
├── Editor/
│ ├── AssetValidation/
│ │ ├── Core/
│ │ │ ├── ValidationFramework.cs
│ │ │ ├── AssetValidator.cs
│ │ │ └── ValidationResult.cs
│ │ ├── Validators/
│ │ │ ├── ModelValidator.cs
│ │ │ ├── MaterialValidator.cs
│ │ │ ├── TextureValidator.cs
│ │ │ └── AnimationValidator.cs
│ │ ├── UI/
│ │ │ ├── AssetValidationWindow.cs
│ │ │ ├── ValidationTreeView.cs
│ │ │ └── ValidationReportView.cs
│ │ ├── Rules/
│ │ │ ├── ValidationRule.cs
│ │ │ └── RuleLibrary.cs
│ │ └── Utilities/
│ │ ├── BatchProcessor.cs
│ │ └── AutoFixSystem.cs
│ └── AssetValidationMenu.cs
├── Configs/
│ └── ValidationRules.asset
└── Documentation/
└── AssetValidationGuide.md
总结
通过本文实现的自动化美术资源校验工具,项目团队可以获得以下收益:
-
质量保障:确保所有美术资源符合项目技术规范
-
效率提升:自动化检查替代繁琐的手动验证
-
知识沉淀:将技术规范转化为可执行的检查规则
-
流程标准化:建立统一的资源质量门禁流程
工具的核心优势在于其可扩展性,通过实现IAssetValidator接口,可以轻松添加新的校验规则,适应不同项目的特定需求。同时,可视化界面和自动化修复功能大大降低了使用门槛,使美术和QA团队都能有效利用该工具。
建议将工具集成到日常开发流程中,如在资源导入时自动校验、在版本提交前强制检查、在CI/CD流水线中作为质量门禁等,从而全面提升项目资源质量。