三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Unity集成Python开发指南:自动化工具与数据处理的实战应用

Unity集成Python开发指南:自动化工具与数据处理的实战应用

1. 项目概述:为什么要在Unity里用Python?

如果你是一个Unity开发者,或者对游戏开发、交互式内容创作感兴趣,那你对C#一定不陌生。Unity的核心脚本语言就是C#,它强大、高效,是构建复杂游戏逻辑的不二之选。但最近几年,我发现在一些特定场景下,把Python引入Unity工作流,能带来意想不到的效率和便利。这听起来可能有点“跨界”,毕竟一个是游戏引擎,一个是数据科学和自动化的宠儿,但它们俩结合,能擦出不少火花。

简单来说,Unity官方从某个版本开始,通过一个名为com.unity.scripting.python的包,正式支持了Python脚本。这可不是让你用Python去写游戏的核心战斗循环或者物理模拟——那种高性能需求的任务还是得交给C#。Python在Unity里的角色,更像是一个强大的“自动化助手”和“数据桥梁”。想象一下,你需要批量处理上百个模型资产的导入设置,或者想用一段复杂的数学公式生成地形高度图,又或者想快速连接一个外部API获取实时数据并驱动游戏内的UI。用C#写这些工具或胶水代码当然可以,但Python凭借其简洁的语法、丰富的库(如NumPy, Pandas, Requests)和强大的脚本能力,往往能让你几行代码就搞定,大大提升了开发管线(Pipeline)的构建效率和原型验证速度。

对于零基础的朋友,你可能会担心:“我既要学Unity,又要学C#,现在还要学Python?” 别慌。这篇指南的目的,就是帮你绕过那些复杂的配置坑,手把手带你从零开始,在Unity中搭建起Python环境,并完成几个有代表性的实用案例。你会发现,即使你之前没写过Python,也能跟着一步步做出能实际运行的东西。我们的目标是“实用”和“轻松上手”,让你快速感受到这种混合编程的魔力。

2. 环境准备与核心原理拆解

在开始写第一行代码之前,我们必须把地基打好。Unity中使用Python,并不是一个“开箱即用”的功能,它依赖于一个独立的包,并且需要正确的Python环境。理解背后的原理,能帮你更好地排查未来可能遇到的问题。

2.1 Unity版本与Python Scripting包

首先,最关键的一步是确认你的Unity版本。官方Python Scripting包是一个“预发布”(Pre-release)包,这意味着它还在积极开发中,可能某些版本存在不稳定。根据官方手册,它从Unity 2021.2版本开始提供较完善的支持。我个人推荐使用Unity 2022.3 LTS(长期支持版)或更新版本,LTS版本更稳定,社区资源也更丰富。

安装包的步骤很简单:

  1. 打开Unity,进入Window -> Package Manager
  2. 在Package Manager窗口左上角,点击“Packages”下拉菜单,选择“Unity Registry”
  3. 在搜索框中输入“Python”。
  4. 你应该能看到一个名为“Python Scripting”的包,注意它的版本状态可能是“Pre-release”。点击它,然后点击右下角的“Install”按钮。

注意:安装这个包可能会花费一些时间,因为它需要下载并集成 Python for .NET(Python.NET)的后端。安装过程中请保持网络通畅,如果失败,可以尝试重启Unity或检查网络代理设置。

安装成功后,你会在Unity编辑器的顶部菜单栏看到一个新的菜单项:Python。这就说明环境的基础部分已经就位了。

2.2 Python环境配置:系统Python vs 嵌入式Python

这是最容易混淆和出错的地方。Unity的Python Scripting包运行Python代码,需要一个本地的Python解释器。它给你两种选择:

  1. 使用系统已安装的Python:如果你电脑上已经装了Python(比如通过python.org或Anaconda安装),Unity可以尝试去找到并使用它。
  2. 使用Unity内置的Python:安装包时,它会自动下载一个特定版本的、精简的Python环境(通常是Python 3.9或3.10),这个环境是便携式的,只供Unity内部使用。

我强烈建议初学者和大多数项目使用“Unity内置的Python”。理由如下:

  • 环境隔离:不会与你系统上其他Python项目所需的库版本冲突。
  • 一致性:团队协作时,所有人的Unity内部Python环境完全一致,避免了“在我机器上好好的”这类问题。
  • 简单:无需你额外配置系统环境变量。

配置路径在Edit -> Preferences(Mac是Unity -> Preferences),然后找到Python选项卡。在这里,你可以设置“Python Interpreter”路径。如果使用内置环境,通常它会自动填好一个类似[YourProjectPath]/Library/PythonInstall的路径。确认这个路径存在即可。

如果你想使用系统Python(比如因为你需要用到一些已经用pip安装好的复杂库),就在这里指定你的python.exe的完整路径,例如C:\Users\YourName\AppData\Local\Programs\Python\Python39\python.exe

2.3 核心原理:Python.NET 是如何工作的?

简单了解一下原理,能让你更清楚Python脚本在Unity里能做什么、不能做什么。Unity的Python支持底层是基于Python for .NET(Python.NET)这个开源项目。

你可以把它想象成一个“翻译官”。当你在Unity里执行Python代码时,发生了以下事情:

  1. Unity的C#核心(.NET运行时)启动了一个Python解释器进程。
  2. Python.NET在中间架起桥梁,让Python代码可以:
    • 调用C#(.NET)的类和方法:这是最重要的能力。意味着你的Python脚本可以创建GameObject、修改组件属性、调用你写的C#方法等。本质上,你是在用Python语法操作Unity引擎的C# API。
    • 被C#调用:你也可以在C#脚本里,启动和执行一段Python脚本,并获取其结果。

但是,这个“翻译”是有成本的。每一次跨越Python和.NET边界的调用(我们称之为“互操作”),都会带来微小的性能开销。因此,切忌在性能敏感的每帧更新(如Update函数)中执行大量的Python代码。Python的定位应该是编辑器工具脚本、数据预处理、批量操作等“一次性”或“低频次”的任务。

3. 第一个Python脚本:从编辑器工具开始

理论说再多,不如动手试一下。让我们创建一个最简单的Python脚本,感受一下它在Unity编辑器里的工作方式。

3.1 创建与执行Python脚本文件

在Unity项目中,Python脚本就是普通的.py文本文件。你可以在Project窗口的任何位置右键 ->Create -> Python Script来创建一个模板文件。不过,直接在你喜欢的位置(比如Assets/Editor/Python文件夹下)新建一个文本文件,然后把后缀改成.py也一样,例如my_first_tool.py

用任何文本编辑器(推荐VSCode、Sublime Text,甚至Unity自带的编辑器)打开这个文件,输入以下代码:

import UnityEngine def say_hello(): # 访问UnityEngine的Debug类,就像在C#里一样 UnityEngine.Debug.Log("Hello from Python!") # 我们甚至可以用Python的字符串格式化 editor_version = UnityEngine.Application.unityVersion UnityEngine.Debug.Log(f"Unity Editor Version: {editor_version}") # 当这个脚本在Unity编辑器中被直接运行时,会执行下面的代码 if __name__ == "__main__": say_hello()

保存文件后,回到Unity编辑器。你有两种方式运行这个脚本:

  1. 菜单栏运行:点击顶部Python菜单,选择Run Python Script,然后在弹出的文件选择框中找到你的my_first_tool.py文件并打开。
  2. 右键菜单运行:在Project窗口中,右键点击你的.py文件,选择Run Python Script

运行后,你应该能在Unity的Console窗口看到输出的两行日志。恭喜!你的第一个Unity Python脚本成功运行了。它通过import UnityEngine导入了核心的Unity引擎API,然后像在C#里一样使用Debug.Log

3.2 构建一个实用的编辑器工具:批量重命名

光是打印日志显然不够实用。让我们做一个真正能提升效率的小工具:批量重命名选中的游戏对象(GameObject)。

Assets/Editor文件夹下创建一个新文件batch_rename.pyAssets/Editor是个特殊文件夹,里面的脚本只在Unity编辑器环境下运行,不会被打包到游戏里,非常适合放工具脚本。

import UnityEngine import re def batch_rename_selected(): """ 批量重命名当前在Hierarchy中选中的所有GameObject。 规则:在原名基础上添加前缀 "Renamed_" """ # 获取当前选中的所有GameObject selected_objects = UnityEngine.Selection.gameObjects if not selected_objects or len(selected_objects) == 0: UnityEngine.Debug.LogWarning("没有选中任何GameObject。") return # 开启一个编辑器操作组,这样所有的重命名操作可以被一次撤销 UnityEngine.Undo.RecordObjects(selected_objects, "Batch Rename") rename_count = 0 for obj in selected_objects: original_name = obj.name # 简单的重命名逻辑:添加前缀 new_name = f"Renamed_{original_name}" obj.name = new_name UnityEngine.Debug.Log(f"Renamed: {original_name} -> {new_name}") rename_count += 1 UnityEngine.Debug.Log(f"批量重命名完成,共处理了 {rename_count} 个对象。") # 刷新编辑器界面,让更改立即显示在Hierarchy中 UnityEngine.EditorApplication.RepaintHierarchyWindow() # 创建编辑器菜单项 # 这行代码会在Unity的菜单栏“Python”下添加一个子菜单项 UnityEngine.MenuItem("Python Tools/Batch Rename Selected Objects") def execute_batch_rename(): batch_rename_selected() if __name__ == "__main__": # 如果直接运行脚本,也执行一次(可选) batch_rename_selected()

这段代码做了几件事:

  1. UnityEngine.Selection.gameObjects获取了你在Hierarchy窗口里选中的物体列表。
  2. UnityEngine.Undo.RecordObjects是关键一步,它告诉Unity的撤销系统,接下来要对这些对象做的修改是一个整体操作。这样你按Ctrl+Z时可以一次性撤销所有重命名,非常专业。
  3. 遍历列表,修改每个对象的name属性。
  4. UnityEngine.MenuItem是一个装饰器(decorator),它把这个Python函数注册成了一个Unity编辑器菜单项。保存脚本后,你会在顶部菜单栏看到Python Tools -> Batch Rename Selected Objects,点击它就能运行。

实操心得

  • 工具脚本放在Assets/Editor下是个好习惯。
  • 涉及修改场景或资源数据的操作,务必使用Undo.RecordObjectUndo.RecordObjects来支持撤销,这是编写可靠编辑器工具的基本素养。
  • UnityEngine.EditorApplication.RepaintHierarchyWindow()可以强制刷新界面,有时修改了数据但界面没更新,调用这个就对了。

你可以基于这个模板扩展出更复杂的功能,比如用正则表达式(re模块)进行模式匹配和替换,或者添加一个编辑器窗口(EditorWindow)来提供图形化参数设置界面。

4. 深入交互:Python与C#脚本的双向通信

Python在Unity里真正的威力,在于它能和你的C#代码“对话”。这意味着你可以用Python快速编写算法原型或数据处理逻辑,然后让C#脚本来驱动游戏运行时的行为。

4.1 在Python中调用自定义C#方法

假设你有一个C#脚本DataProcessor.cs,它挂载在一个GameObject上,里面有一个处理数据的方法。

// DataProcessor.cs using UnityEngine; public class DataProcessor : MonoBehaviour { // 一个可以被Python调用的静态方法 public static int AddNumbers(int a, int b) { Debug.Log($"C#: Adding {a} and {b}"); return a + b; } // 一个实例方法,需要先获取到对象实例 public string Greet(string name) { return $"Hello, {name} from C# instance!"; } }

现在,我们写一个Python脚本call_csharp.py来调用它:

import UnityEngine import clr # Python.NET 的核心模块,用于加载.NET程序集 # 1. 调用静态方法非常简单,就像调用UnityEngine的API一样 result_static = UnityEngine.DataProcessor.AddNumbers(5, 3) UnityEngine.Debug.Log(f"Python got result from static method: {result_static}") # 2. 调用实例方法,需要先找到场景中的GameObject和组件 # 假设场景中有一个GameObject名字叫“Processor”,上面挂了DataProcessor脚本 game_object = UnityEngine.GameObject.Find("Processor") if game_object: # 获取组件 data_processor_component = game_object.GetComponent[UnityEngine.DataProcessor]() if data_processor_component: result_instance = data_processor_component.Greet("Python Developer") UnityEngine.Debug.Log(f"Python got result from instance method: {result_instance}") else: UnityEngine.Debug.LogError("DataProcessor component not found on the GameObject.") else: UnityEngine.Debug.LogWarning("GameObject 'Processor' not found in the scene.")

关键点在于GetComponent[UnityEngine.DataProcessor]()这里的方括号语法,这是Python.NET调用泛型方法的方式。它等同于C#中的GetComponent<DataProcessor>()

4.2 在C#中启动并获取Python脚本结果

反过来,我们也可以在C#里,把一些计算密集型或需要特定Python库的任务,交给Python脚本去完成。

首先,我们需要在Unity中启用对UnityEditor.Scripting.Python命名空间的访问(这仅用于编辑器脚本)。然后,我们可以这样写一个C#编辑器工具:

// PythonRunner.cs - 放在Assets/Editor文件夹下 using UnityEditor; using UnityEditor.Scripting.Python; // 需要引用这个命名空间 using UnityEngine; public class PythonRunner : EditorWindow { [MenuItem("Python Tools/Run External Python Script")] static void RunPythonScript() { // 打开文件面板,让用户选择一个.py文件 string scriptPath = EditorUtility.OpenFilePanel("Select Python Script", Application.dataPath, "py"); if (string.IsNullOrEmpty(scriptPath)) { Debug.Log("No script selected."); return; } // 使用PythonRunner来执行脚本 // PythonRunner.RunFile 是同步执行的,会阻塞编辑器直到脚本完成 try { PythonRunner.RunFile(scriptPath); Debug.Log($"Python script executed: {scriptPath}"); } catch (System.Exception e) { Debug.LogError($"Failed to run Python script: {e.Message}"); } } }

但是,PythonRunner.RunFile是“执行”脚本,我们如何获取Python脚本计算后的返回值呢?一个更常见的模式是让Python脚本将结果保存到一个双方约定的地方(比如一个临时文件,或者一个静态的C#类属性中),然后C#再去读取。

更优雅的双向通信模式: 我们可以定义一个静态的C#类作为“数据交换站”。

// PythonBridge.cs - 可以放在Assets/Scripts下,不需要挂载 public static class PythonBridge { public static string LastPythonResult { get; set; } public static int CalculationResult { get; set; } }

然后在Python脚本中,我们直接设置这个静态类的属性:

# calculate_and_store.py import UnityEngine # 假设我们进行了一些复杂的计算(这里用简单计算模拟) complex_result = 42 * 100 # 将结果存放到C#的静态类中 UnityEngine.PythonBridge.CalculationResult = complex_result UnityEngine.PythonBridge.LastPythonResult = f"Calculation done at {UnityEngine.Time.time}" UnityEngine.Debug.Log("Python calculation completed and result stored in bridge.")

最后,在C#的任何地方(比如一个按钮的点击事件里),你都可以读取PythonBridge.CalculationResult来使用这个值。这种方式实现了松耦合的异步通信。

5. 利用Python生态:数据处理与自动化实战

Python最大的优势是其庞大的第三方库生态系统。在Unity中,我们可以利用这些库来处理游戏开发中繁琐的数据任务。

5.1 使用Pandas读取和配置游戏数据

假设你有一个平衡设计师用Excel维护的游戏角色属性表(CharacterStats.xlsx),你需要将这些数据导入到Unity中,并生成对应的ScriptableObject或直接配置到Prefab上。

步骤:

  1. 准备Python环境:Unity内置的Python是精简版,默认没有Pandas。我们需要为它安装库。
    • 打开系统命令行(终端),关键一步:需要先激活Unity项目所用的Python环境。找到你项目下的Python解释器路径(在Preferences -> Python里可以看到),例如YourProject/Library/PythonInstall/python.exe
    • 使用这个python的pip进行安装。命令类似:"YourProject/Library/PythonInstall/python.exe" -m pip install pandas openpyxl
    • openpyxl是用于读取Excel文件的引擎。
  2. 编写数据处理脚本excel_to_unity.py:
import UnityEngine import pandas as pd import os import json def import_character_stats(excel_path): """ 从Excel文件读取角色数据,并打印或转换为Unity可用的格式。 """ # 使用pandas读取Excel df = pd.read_excel(excel_path, sheet_name='Sheet1') # 假设数据在Sheet1 UnityEngine.Debug.Log(f"成功读取Excel文件,共 {len(df)} 行数据。") UnityEngine.Debug.Log(f"列名: {list(df.columns)}") # 示例:将DataFrame转换为字典列表,方便后续处理 characters_data = df.to_dict('records') # 在这里,你可以遍历 characters_data,然后: # 1. 动态创建GameObject和组件并赋值。 # 2. 或者,更常见的做法是生成一个JSON文件,由C#去读取。 # 3. 或者,直接调用你写好的C#方法,将数据注入到现有的管理器中。 # 示例:保存为JSON到Assets目录下 output_path = UnityEngine.Application.dataPath + "/Resources/CharacterData.json" with open(output_path, 'w', encoding='utf-8') as f: json.dump(characters_data, f, ensure_ascii=False, indent=2) UnityEngine.Debug.Log(f"角色数据已导出到: {output_path}") # 刷新Asset Database,让Unity立刻识别新生成的JSON文件 UnityEngine.AssetDatabase.Refresh() # 使用示例:你需要将下面的路径替换成你Excel文件的实际路径 if __name__ == "__main__": # 假设Excel文件放在Assets同级目录的Data文件夹里 project_path = os.path.dirname(UnityEngine.Application.dataPath) excel_file = os.path.join(project_path, "Data", "CharacterStats.xlsx") if os.path.exists(excel_file): import_character_stats(excel_file) else: UnityEngine.Debug.LogError(f"Excel文件未找到: {excel_file}")

这个脚本展示了如何利用Pandas轻松处理结构化数据,并输出为Unity容易加载的JSON格式。你可以将这个脚本设置为一个定期执行的编辑器工具,每当Excel表格更新,运行一下脚本就能同步游戏数据。

5.2 使用Requests库获取网络数据驱动游戏内容

想象一个场景:你的游戏需要显示实时天气,或者从服务器获取一些动态配置。你可以用Python快速写一个原型。

# fetch_weather.py import UnityEngine import requests import json def fetch_weather_data(api_url): """ 从公开的天气API获取数据。(请替换为真实可用的API) 这是一个示例,演示网络请求。 """ try: # 发送GET请求 response = requests.get(api_url, timeout=10) response.raise_for_status() # 如果状态码不是200,抛出异常 weather_info = response.json() # 解析数据,这里根据实际API结构来写 city = weather_info.get('name', 'Unknown') temp = weather_info.get('main', {}).get('temp', 0) description = weather_info.get('weather', [{}])[0].get('description', 'clear sky') result_text = f"Weather in {city}: {temp}°C, {description}" UnityEngine.Debug.Log(result_text) # 将结果存储到我们之前定义的桥接类中,供C#使用 UnityEngine.PythonBridge.LastPythonResult = result_text return weather_info except requests.exceptions.RequestException as e: UnityEngine.Debug.LogError(f"网络请求失败: {e}") return None except json.JSONDecodeError as e: UnityEngine.Debug.LogError(f"解析JSON失败: {e}") return None # 示例API(这是一个免费的示例API,可能不稳定,请自行寻找可靠的测试API) # 例如:https://api.openweathermap.org/data/2.5/weather?q=London&appid=YOUR_API_KEY&units=metric if __name__ == "__main__": # !!!重要:使用真实API时,请勿将密钥硬编码在脚本中!!! # 应该从环境变量或Unity的PlayerPrefs/配置文件中读取。 sample_url = "https://api.openweathermap.org/data/2.5/weather?q=London&appid=your_api_key_here&units=metric" UnityEngine.Debug.LogWarning("请替换sample_url中的API密钥和城市参数。") # fetch_weather_data(sample_url) # 取消注释并配置后运行

注意事项

  • 网络权限:在Unity编辑器内运行网络请求通常没问题。但如果你的最终游戏需要联网,并且使用了从Python脚本生成的代码或数据,你需要确保目标平台(如iOS、Android)有正确的网络权限设置,并且考虑使用Unity原生的UnityWebRequest进行最终实现以获得更好的兼容性和控制力。Python脚本在这里更适合做编辑器下的数据抓取和预处理。
  • 依赖管理:像requests这样的库也需要用pip安装到Unity的Python环境中。
  • 安全:绝对不要将API密钥等敏感信息直接写在脚本里。对于编辑器工具,可以将其保存在系统的环境变量中,Python通过os.environ读取。

6. 常见问题与排查技巧实录

在实际操作中,你肯定会遇到各种报错。这里我整理了几个最常见的问题和解决方法,都是我踩过坑后总结出来的。

6.1 Python环境与包管理问题

问题1:ModuleNotFoundError: No module named ‘xxx’这是最常见的问题,意味着Python解释器找不到你import的模块。

  • 原因A:你用的是Unity内置Python,但没安装那个包。
    • 解决:用Unity内置Python的pip安装。找到解释器路径(Preferences -> Python),在命令行中用完整路径执行安装,例如"项目路径/Library/PythonInstall/python.exe" -m pip install pandas
  • 原因B:你指定了系统Python,但包安装在另一个Python版本或虚拟环境中。
    • 解决:在Unity的Python偏好设置中确认解释器路径是否正确指向了你安装包的那个Python环境。或者,在正确的环境中重新安装包。

问题2:安装包时速度慢或超时

  • 解决:使用国内镜像源。在pip安装命令后添加-i参数,例如:
    "项目路径/Library/PythonInstall/python.exe" -m pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple

6.2 脚本执行与语法错误

问题3:运行脚本时无任何反应,Console也没有报错

  • 可能原因:脚本中有语法错误,导致Python解释器在解析阶段就失败了,有时Unity不会捕获这种错误并打印到Console。
  • 排查:尝试在命令行中直接用Unity的Python解释器运行你的脚本,可以看到更详细的错误信息。
    "项目路径/Library/PythonInstall/python.exe" "你的脚本路径.py"

问题4:AttributeError: type object ‘XXX’ has no attribute ‘YYY’

  • 可能原因:在Python中调用C#的API时,方法名或属性名拼写错误,或者该API在你当前使用的Unity版本中不存在。
  • 排查
    1. 仔细检查拼写,注意C#的公共方法/属性在Python中通常是同名的。
    2. 去Unity官方脚本API文档(https://docs.unity3d.com/ScriptReference/ )确认该API是否存在于你的Unity版本中。Python能调用的,基本都是C# API的映射。

6.3 性能与使用场景误区

问题5:在游戏运行时(Play Mode)使用Python脚本导致卡顿

  • 原因与建议:这是最重要的一个坑。Python和.NET的互操作调用(Marshalling)是有开销的。绝对不要在Update()FixedUpdate()或任何每帧执行的游戏逻辑循环中调用复杂的Python代码
  • 正确姿势
    • 编辑器扩展:Python的主场。用于制作资源导入处理工具、批量修改器、数据转换脚本等。这些只在编辑时运行。
    • 离线数据处理:在游戏启动前(如打包流程中),用Python脚本预处理数据,生成Unity可直接读取的资产(如JSON, AssetBundles)。
    • 低频后台任务:如果必须在运行时使用,应将其放在单独的线程或协程中,并且仅用于低频、非实时性的任务,例如解析一份从网上下载的配置数据。

6.4 调试技巧

打印日志UnityEngine.Debug.Log()是你最好的朋友。在Python脚本中大量使用它来输出变量状态和执行路径。使用Python原生打印print()语句的输出有时会重定向到Unity的Console,但不如Debug.Log可靠。建议统一使用Debug.Log在外部IDE调试(高级):可以配置VSCode或PyCharm来调试附加到Unity进程的Python脚本,但这设置起来较为复杂。对于大多数工具脚本,充分的日志输出已经足够定位问题。

7. 项目实战:构建一个自动化资源检查器

让我们综合运用以上知识,创建一个有点实际用处的工具:一个自动扫描项目指定文件夹(如Assets/Art/Textures),检查纹理尺寸是否为2的幂(PoT,许多图形API的要求)、格式是否正确,并生成报告的工具。

这个工具将涉及:遍历文件系统、使用Unity的AssetDatabase API获取纹理信息、使用Python进行逻辑判断和报告生成。

# texture_audit_tool.py import UnityEngine import os import sys from datetime import datetime def check_texture_settings(texture_path): """检查单个纹理的设置""" # 通过AssetDatabase加载纹理为TextureImporter对象,这样才能获取导入设置 importer = UnityEngine.AssetImporter.GetAtPath(texture_path) as UnityEngine.TextureImporter if not importer: return None asset = UnityEngine.AssetDatabase.LoadAssetAtPath(texture_path, UnityEngine.Texture2D) if not asset: return None issues = [] # 检查尺寸是否为2的幂 width = asset.width height = asset.height def is_power_of_two(n): return (n & (n - 1)) == 0 and n != 0 if not is_power_of_two(width) or not is_power_of_two(height): issues.append(f"非2的幂尺寸: {width}x{height}") # 检查纹理类型设置(例如,普通纹理还是法线贴图) # 这里只是示例,你可以根据项目规范检查更多设置,如sRGB、压缩格式等 if importer.textureType != UnityEngine.TextureImporterType.Default: # 假设我们要求所有漫射贴图都是Default类型 issues.append(f"纹理类型为 {importer.textureType},非Default") # 检查Max Size是否合适(假设我们要求不超过2048) if importer.maxTextureSize > 2048: issues.append(f"Max Size设置过大: {importer.maxTextureSize}") return { 'path': texture_path, 'width': width, 'height': height, 'importer_type': str(importer.textureType), 'max_size': importer.maxTextureSize, 'issues': issues, 'has_issues': len(issues) > 0 } def audit_textures_in_folder(folder_path="Assets/Art/Textures"): """审计指定文件夹下的所有纹理""" UnityEngine.Debug.Log(f"开始审计纹理文件夹: {folder_path}") if not UnityEngine.AssetDatabase.IsValidFolder(folder_path): UnityEngine.Debug.LogError(f"文件夹无效: {folder_path}") return # 获取文件夹下所有资产GUID guids = UnityEngine.AssetDatabase.FindAssets("t:Texture2D", [folder_path]) audit_results = [] problematic_textures = [] for guid in guids: asset_path = UnityEngine.AssetDatabase.GUIDToAssetPath(guid) # 只处理特定后缀,排除.meta文件等 if asset_path.lower().endswith(('.png', '.jpg', '.jpeg', '.tga', '.psd', '.bmp')): result = check_texture_settings(asset_path) if result: audit_results.append(result) if result['has_issues']: problematic_textures.append(result) # 生成报告 report_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") report_lines = [] report_lines.append(f"=== 纹理审计报告 ===") report_lines.append(f"时间: {report_time}") report_lines.append(f"扫描文件夹: {folder_path}") report_lines.append(f"扫描纹理数量: {len(audit_results)}") report_lines.append(f"存在问题纹理: {len(problematic_textures)}") report_lines.append("") report_lines.append("--- 问题详情 ---") for tex in problematic_textures: report_lines.append(f"纹理: {tex['path']}") report_lines.append(f" 尺寸: {tex['width']}x{tex['height']}") for issue in tex['issues']: report_lines.append(f" - {issue}") report_lines.append("") # 空行分隔 report_text = "\n".join(report_lines) # 输出到Unity Console for line in report_lines: if "问题详情" in line or "===" in line or "---" in line: UnityEngine.Debug.LogWarning(line) # 用Warning级别高亮标题 else: UnityEngine.Debug.Log(line) # 将报告保存为文本文件 report_file_path = UnityEngine.Application.dataPath + "/../TextureAuditReport.txt" try: with open(report_file_path, 'w', encoding='utf-8') as f: f.write(report_text) UnityEngine.Debug.Log(f"完整报告已保存至: {report_file_path}") except Exception as e: UnityEngine.Debug.LogError(f"保存报告文件失败: {e}") # 在Project窗口高亮显示有问题的纹理(可选) if problematic_textures: selection_guids = [] for tex in problematic_textures: guid = UnityEngine.AssetDatabase.AssetPathToGUID(tex['path']) if guid: selection_guids.append(guid) # 这里需要调用一个内部API来高亮选择,操作较复杂,通常输出路径就够了。 UnityEngine.Debug.Log("请在Console中查看问题纹理路径。") return problematic_textures # 创建菜单项 UnityEngine.MenuItem("Assets/Audit Selected Textures", priority=100) def audit_selected_textures_menu(): """审计当前在Project窗口选中的纹理""" selected_objects = UnityEngine.Selection.objects texture_paths = [] for obj in selected_objects: if isinstance(obj, UnityEngine.Texture2D): path = UnityEngine.AssetDatabase.GetAssetPath(obj) if path: texture_paths.append(path) if not texture_paths: # 如果没有选中纹理,或者选中的是文件夹,则审计整个选中文件夹 selected_paths = [UnityEngine.AssetDatabase.GetAssetPath(obj) for obj in selected_objects if obj] for path in selected_paths: if UnityEngine.AssetDatabase.IsValidFolder(path): audit_textures_in_folder(path) return UnityEngine.Debug.LogWarning("请选中一个文件夹或若干纹理文件。") return # 审计选中的纹理 for path in texture_paths: result = check_texture_settings(path) if result and result['has_issues']: for issue in result['issues']: UnityEngine.Debug.LogWarning(f"{path}: {issue}") UnityEngine.MenuItem("Tools/Python/Audit All Textures in Project") def audit_all_textures_menu(): """审计整个项目中的所有纹理(谨慎使用,项目大时可能很慢)""" if UnityEngine.EditorUtility.DisplayDialog("纹理审计", "这将扫描项目中所有纹理,可能耗时较长。确认继续?", "是", "否"): # 可以递归扫描所有文件夹,这里简单起见扫描Assets根目录 guids = UnityEngine.AssetDatabase.FindAssets("t:Texture2D") UnityEngine.Debug.Log(f"开始审计全项目纹理,共找到 {len(guids)} 个纹理。") # 实际处理... 为避免卡死编辑器,对于大量资产应考虑分帧处理或进度条。 UnityEngine.Debug.LogWarning("全项目审计功能较耗时,建议使用文件夹审计。") if __name__ == "__main__": # 默认运行:审计常用的纹理文件夹 audit_textures_in_folder("Assets/Textures")

这个实战项目涵盖了多个关键点:

  1. 资产路径操作:使用AssetDatabase相关API。
  2. 类型检查与转换:Python中使用as进行类型转换。
  3. 业务逻辑封装:将检查逻辑单独写成函数。
  4. 报告生成:结合Python的字符串和文件操作,生成易于阅读的文本报告。
  5. 编辑器集成:通过MenuItem创建了右键菜单和Tools菜单,方便使用。

你可以根据需要扩展检查规则,例如检查MipMap是否开启、压缩格式是否为ASTC等。这个工具能帮助美术和开发人员在项目早期发现资源规范问题,避免在打包或运行时出现意外。

← 返回列表