这两年我对 AI 在 Unity 开发里最稳定的判断之一是:
别一上来就指望它帮你写完整玩法。
但让它帮你写编辑器工具,很多时候是真的顺手。
原因很简单。
编辑器工具通常有几个特点:
- 输入输出边界清楚
- 交互流程不算太复杂
- 很多代码是重复样板
- 容易人工 review
这几条刚好都比较适合 AI 发挥。
所以如果让我选一个最值得先落地的 Unity + AI 场景,我通常会把编辑器工具排得很靠前。
一、为什么 AI 特别适合帮忙写 Unity 编辑器工具
因为这类代码,很多时候难点不在“算法有多深”,而在于:
- API 记不记得全
- 细节是否写完整
- UI、菜单、选择集、Undo、AssetDatabase 这些样板有没有漏
而这些恰好是 AI 擅长补齐的地方。
比如你脑子里已经很清楚要做一个工具:
- 扫描场景里的 Image
- 找出没关 Raycast Target 的组件
- 一键修复
- 输出修改日志
这种任务如果手写,当然也不难。
但你得自己把菜单入口、滚动列表、按钮事件、批量修改、脏标记这些细节都补上。
AI 在这里最大的价值,就是先帮你把 60 分版本快速搭起来。
二、最常见的低效提示词:帮我写个 Unity 工具
这类提示词的问题不是 AI 完全写不出来。
而是它太模糊了。
模糊输入通常会得到一个“看上去像能跑,但细节不太贴项目”的答案。
比如它可能:
- UI 入口不对
- API 用法过时
- 没考虑 Undo
- 没考虑 Prefab 资源修改
- 没考虑场景对象和资源对象的区别
最后你不是省事,而是在帮它返工。
所以提示词别太偷懒。
三、我更推荐的提示词写法:把上下文和约束一次说清楚
如果我要让 AI 帮我写一个 Unity 编辑器工具,我通常会把这些东西直接说出来:
- Unity 版本
- 要跑在运行时还是 Editor
- 操作对象是什么
- 输入和输出是什么
- 需要哪些按钮或界面元素
- 是否要支持 Undo
- 是否要兼容批量处理
- 返回代码时希望的结构
比如这个提示词,我觉得就比“帮我写个工具”强很多:
1 2 3 4 5 6 7 8 9 10 11 12
| 你是资深 Unity 工具开发工程师。 请帮我写一个 Unity EditorWindow,Unity 版本 2021 LTS,C#。
工具目标:扫描当前打开场景中所有 Graphic 组件,找出 Raycast Target = true 但不在 Button、Toggle、Slider 等交互链路上的对象。
要求: 1. 提供 Scan 和 Fix Selected 两个按钮。 2. 扫描结果用滚动列表展示,显示对象路径、组件类型、当前 Raycast Target 状态。 3. 修复时要支持 Undo.RecordObject。 4. 修复后调用 EditorUtility.SetDirty。 5. 代码拆成一个 EditorWindow 和必要的辅助方法,不要写成一个超长函数。 6. 先给完整代码,再解释关键实现点和可能遗漏的边界情况。
|
这种提示词的好处很直接。
AI 不是在猜你想干什么,而是在补实现。
四、一个比较实用的例子:扫描并修复无效 Raycast Target
下面这段代码,就是典型适合让 AI 先起草、再人工审一下的编辑器工具类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
| using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI;
public class RaycastTargetScannerWindow : EditorWindow { private readonly List<Graphic> _results = new List<Graphic>(); private Vector2 _scroll;
[MenuItem("Tools/UI/Raycast Target Scanner")] private static void Open() { GetWindow<RaycastTargetScannerWindow>("Raycast Scanner"); }
private void OnGUI() { EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Scan", GUILayout.Height(30))) { ScanScene(); }
using (new EditorGUI.DisabledScope(_results.Count == 0)) { if (GUILayout.Button("Fix Selected", GUILayout.Height(30))) { FixResults(); } }
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(); EditorGUILayout.LabelField($"Result Count: {_results.Count}", EditorStyles.boldLabel);
_scroll = EditorGUILayout.BeginScrollView(_scroll);
foreach (var graphic in _results) { if (graphic == null) { continue; }
EditorGUILayout.BeginVertical("box"); EditorGUILayout.ObjectField("Target", graphic, typeof(Graphic), true); EditorGUILayout.LabelField("Path", GetHierarchyPath(graphic.transform)); EditorGUILayout.LabelField("Type", graphic.GetType().Name); EditorGUILayout.EndVertical(); }
EditorGUILayout.EndScrollView(); }
private void ScanScene() { _results.Clear();
var graphics = SceneManager.GetActiveScene() .GetRootGameObjects() .SelectMany(root => root.GetComponentsInChildren<Graphic>(true));
foreach (var graphic in graphics) { if (!graphic.raycastTarget) { continue; }
if (HasInteractiveParent(graphic.transform)) { continue; }
_results.Add(graphic); } }
private void FixResults() { foreach (var graphic in _results) { if (graphic == null) { continue; }
Undo.RecordObject(graphic, "Disable Raycast Target"); graphic.raycastTarget = false; EditorUtility.SetDirty(graphic); } }
private static bool HasInteractiveParent(Transform target) { return target.GetComponentInParent<Button>(true) != null || target.GetComponentInParent<Toggle>(true) != null || target.GetComponentInParent<Slider>(true) != null || target.GetComponentInParent<Scrollbar>(true) != null || target.GetComponentInParent<InputField>(true) != null; }
private static string GetHierarchyPath(Transform current) { if (current == null) { return string.Empty; }
var names = new List<string>();
while (current != null) { names.Add(current.name); current = current.parent; }
names.Reverse(); return string.Join("/", names); } }
|
这类代码最适合 AI 先给骨架,再由人补边界。
五、AI 给出的第一版,通常最该人工检查哪几件事
这是重点。
别因为工具代码看上去短,就直接复制进项目。
至少先看这几条:
1. API 是否符合你项目的 Unity 版本
有些回答会混进旧版或新版 API,表面没问题,实际一编译就报错。
2. 是否正确处理 Undo 和脏标记
这是很多 AI 初稿容易漏的地方。
3. 是否区分了场景对象和 Prefab 资源
这两个处理路径经常不一样,别混着改。
4. 是否有明显的空引用和边界遗漏
比如目标对象被删了、结果列表没刷新、组件类型判断太粗。
5. 是否把“项目规则”偷换成了“通用猜测”
比如它默认某些 Graphic 不该开 Raycast,但你项目里恰好有特殊交互用法。
这时候就得你自己拍板。
六、AI 在这类任务里的真正价值,是压缩“从想法到首版工具”的时间
我觉得这件事要说清楚。
AI 不是在替你做技术判断。
它更像一个能先把机械部分铺开的工具助手。
尤其这些任务,它特别合适:
- 批量扫描类工具
- 配置校验类工具
- 资源规范检查工具
- 一次性数据整理窗口
- 简单的可视化辅助面板
这些东西过去也能手写。
但现在完全可以先让 AI 起一版,再自己把结构和边界收紧。
七、我自己比较常用的第二轮提示词
很多人第一次让 AI 生成代码后,就直接结束了。
其实第二轮追问经常更值钱。
比如:
1 2 3 4 5 6 7
| 请继续审查你刚才给出的 Unity 编辑器工具代码。 从以下角度补充: 1. 哪些地方可能在大型项目里误伤资源。 2. 哪些地方应该补 null 检查或异常保护。 3. 如果要支持 Prefab Stage,需要改哪些逻辑。 4. 如果扫描对象数量很多,界面卡顿可以怎么优化。 5. 请只输出改进建议和需要替换的函数,不要重写整份代码。
|
这种问法会比“再优化一下”有效得多。
因为它逼着 AI 往工程边界上靠,而不是继续空泛表演。
八、最后一句
AI 帮 Unity 写编辑器工具,真正好用的地方,不是“一句话生成神器”。
而是它能把很多原本机械、琐碎、容易拖延的小工具开发,先推到一个能 review、能修改、能落地的起点。
前提是你自己得把需求说清楚。
不然 AI 也只能一边猜,一边认真地胡说八道。