execute_code 实战手册:8 个 Unity Editor 自动化场景,每个 30 行 C# 搞定
上一篇介绍了 Funplay Unity MCP 中 execute_code 工具的设计动因——用一个支持任意 C# 片段在 Editor 内存中编译执行的入口,承接所有"专用工具组合不出来"的需求。本文承接,挑 8 个真实场景把"30 行 C# 干完一件 Unity Editor 苦活"这件事坐实。
每个场景都按 IFunplayCommand 模板写,跑在 Editor 主线程,自动接 Undo,结构化返回。脚本可以直接通过 execute_code 工具调用,也能贴进 Editor 菜单脚本里独立运行。
调用约定
所有示例共享同一前置 using:
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
using UnityEditor;
using Funplay.Editor.Tools.Scripting;
每段代码都是一个 class CommandScript : IFunplayCommand,只贴 Execute(ExecutionContext ctx) 方法体。完整脚本请把方法体填进类模板。
场景 1:批量给场景里的 GameObject 加前缀
需求:场景里散落了上百个测试用 GameObject,临测试结束前需要统一改名带 [OBSOLETE] 前缀,方便后续清理。
public void Execute(ExecutionContext ctx)
{
const string prefix = "[OBSOLETE] ";
var targets = Object.FindObjectsByType<GameObject>(FindObjectsSortMode.None)
.Where(g => !g.name.StartsWith(prefix))
.ToList();
foreach (var go in targets)
{
ctx.RegisterObjectModification(go);
go.name = prefix + go.name;
}
EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());
ctx.Log("Renamed {0} GameObjects with prefix", targets.Count);
ctx.ReturnValue = new { renamed = targets.Count };
}
RegisterObjectModification 让用户能 Ctrl+Z 一键全部撤销。Filter 里跳过已加前缀的对象,幂等运行。
场景 2:找出场景里挂了缺失脚本的 GameObject
需求:项目重构后某些脚本被删,场景里残留 missing script 引用,会导致 prefab 序列化警告但不易定位。
public void Execute(ExecutionContext ctx)
{
var bad = new List<object>();
foreach (var go in Object.FindObjectsByType<GameObject>(FindObjectsSortMode.None))
{
var components = go.GetComponents<Component>();
for (int i = 0; i < components.Length; i++)
{
if (components[i] == null)
{
bad.Add(new
{
instanceId = go.GetInstanceID(),
path = AnimationUtility.CalculateTransformPath(go.transform, null),
slot = i
});
}
}
}
ctx.Log("Found {0} GameObjects with missing scripts", bad.Count);
ctx.ReturnValue = new { count = bad.Count, items = bad };
}
返回的每条记录带 instanceId,AI 客户端拿到后可以接着用 find_method=by_id 打开对象进一步处理,不必重新按名字匹配。
场景 3:导出场景层级到 JSON
需求:把场景里某棵子树的层级、组件类型清单导出,给文档系统或外部工具消费。
public void Execute(ExecutionContext ctx)
{
var root = Selection.activeGameObject;
if (root == null)
{
ctx.LogError("请先在 Hierarchy 中选中一个根对象");
return;
}
object Walk(Transform t) => new
{
name = t.name,
instanceId = t.gameObject.GetInstanceID(),
components = t.gameObject.GetComponents<Component>()
.Where(c => c != null).Select(c => c.GetType().Name).ToArray(),
children = Enumerable.Range(0, t.childCount).Select(i => Walk(t.GetChild(i))).ToArray()
};
var tree = Walk(root.transform);
var json = JsonUtility.ToJson(tree, true);
var path = $"Assets/Generated/{root.name}.hierarchy.json";
Directory.CreateDirectory("Assets/Generated");
File.WriteAllText(path, JsonUtility.ToJson(tree, true));
AssetDatabase.ImportAsset(path);
ctx.Log("Exported hierarchy to {0}", path);
ctx.ReturnValue = new { path, rootName = root.name };
}
注:
JsonUtility不支持匿名类,实际项目中建议替换为Newtonsoft.Json.JsonConvert.SerializeObject(Funplay 包内已引用)。
场景 4:统计当前场景所有 MeshFilter 的三角形总数
需求:性能审查阶段快速判断场景的几何复杂度,定位高 poly 区域。
public void Execute(ExecutionContext ctx)
{
var heavy = new List<object>();
long total = 0;
foreach (var mf in Object.FindObjectsByType<MeshFilter>(FindObjectsSortMode.None))
{
var mesh = mf.sharedMesh;
if (mesh == null) continue;
long tri = mesh.triangles.Length / 3;
total += tri;
if (tri > 5000)
{
heavy.Add(new
{
instanceId = mf.gameObject.GetInstanceID(),
name = mf.gameObject.name,
triangles = tri,
meshName = mesh.name
});
}
}
heavy = heavy.OrderByDescending(h => (long)h.GetType().GetProperty("triangles").GetValue(h)).Take(10).ToList();
ctx.Log("Total triangles in scene: {0}", total);
ctx.ReturnValue = new { totalTriangles = total, top10 = heavy };
}
返回里 top10 直接是按三角形数降序的对象列表,AI 客户端可以基于此提出"建议 LOD"或"建议合批"的下一步动作。
场景 5:批量把所有 Material 的 _Color 改成同一个颜色
需求:项目主题切换,临时把所有非 PBR Material 的主色调统一改成调试色,便于视觉验收。
public void Execute(ExecutionContext ctx)
{
var color = new Color(0.2f, 0.6f, 0.9f, 1f);
var guids = AssetDatabase.FindAssets("t:Material", new[] { "Assets/Materials" });
int updated = 0;
foreach (var guid in guids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var mat = AssetDatabase.LoadAssetAtPath<Material>(path);
if (mat == null || !mat.HasProperty("_Color")) continue;
ctx.RegisterObjectModification(mat);
mat.SetColor("_Color", color);
updated++;
}
AssetDatabase.SaveAssets();
ctx.Log("Updated _Color on {0} materials", updated);
ctx.ReturnValue = new { updated, color = ColorUtility.ToHtmlStringRGBA(color) };
}
AssetDatabase.FindAssets 配合路径过滤精准定位资产范围,避免误改第三方包内的 Material。
场景 6:找出未被任何场景引用的 prefab
需求:项目长期演进会留下大量"孤儿 prefab"——已经没有任何场景或其他 prefab 在用了。批量找出来便于评估清理。
public void Execute(ExecutionContext ctx)
{
var allPrefabs = AssetDatabase.FindAssets("t:Prefab")
.Select(AssetDatabase.GUIDToAssetPath)
.ToHashSet();
var referenced = new HashSet<string>();
foreach (var scenePath in AssetDatabase.FindAssets("t:Scene").Select(AssetDatabase.GUIDToAssetPath))
{
foreach (var dep in AssetDatabase.GetDependencies(scenePath, true))
if (allPrefabs.Contains(dep)) referenced.Add(dep);
}
foreach (var prefabPath in allPrefabs.ToList())
{
foreach (var dep in AssetDatabase.GetDependencies(prefabPath, true))
if (allPrefabs.Contains(dep) && dep != prefabPath) referenced.Add(dep);
}
var orphans = allPrefabs.Except(referenced).OrderBy(p => p).ToList();
ctx.Log("Found {0} orphan prefabs out of {1} total", orphans.Count, allPrefabs.Count);
ctx.ReturnValue = new { total = allPrefabs.Count, orphans };
}
AssetDatabase.GetDependencies(path, true) 递归收集依赖,把 prefab 嵌套引用也算进去。在 10K+ 资产的项目里 1-2 秒可以扫完。
场景 7:从 CSV 批量生成 ScriptableObject 配置
需求:策划维护一份 Assets/Data/items.csv,每行一条 ItemConfig 数据,需要把它生成为运行时可加载的 ScriptableObject 资产。
public void Execute(ExecutionContext ctx)
{
var csvPath = "Assets/Data/items.csv";
if (!File.Exists(csvPath))
{
ctx.LogError("CSV not found: {0}", csvPath);
return;
}
Directory.CreateDirectory("Assets/Generated/Items");
var lines = File.ReadAllLines(csvPath).Skip(1); // 跳过表头
int created = 0;
foreach (var line in lines)
{
var cols = line.Split(',');
if (cols.Length < 3) continue;
var so = ScriptableObject.CreateInstance<ItemConfig>();
so.id = cols[0];
so.displayName = cols[1];
so.basePrice = int.Parse(cols[2]);
var path = $"Assets/Generated/Items/{so.id}.asset";
AssetDatabase.CreateAsset(so, path);
ctx.RegisterObjectCreation(so);
created++;
}
AssetDatabase.SaveAssets();
ctx.Log("Created {0} ItemConfig assets", created);
ctx.ReturnValue = new { created, outputDir = "Assets/Generated/Items" };
}
ItemConfig 假设是项目里已有的 ScriptableObject 子类。AssetDatabase.CreateAsset 创建后通过 RegisterObjectCreation 注册到 Undo 栈,撤销时连同资产一起删除。
场景 8:一键禁用场景里所有 AudioSource
需求:调试期间烦人的 BGM、SFX 想集中关掉,又不想逐个禁用 AudioSource 组件。
public void Execute(ExecutionContext ctx)
{
var sources = Object.FindObjectsByType<AudioSource>(FindObjectsSortMode.None);
foreach (var src in sources)
{
ctx.RegisterObjectModification(src);
src.enabled = false;
}
EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());
ctx.Log("Disabled {0} AudioSource components", sources.Length);
ctx.ReturnValue = new { disabled = sources.Length };
}
简短到极致——但比手动逐个禁用快得多。同样自动接 Undo,调试结束后一次 Ctrl+Z 全部恢复。
模式总结
8 个场景按操作类型可以归到三类典型模式:
| 模式 | 特征 | 关键 API |
|---|---|---|
| 查询/审计 | 只读,返回结构化结果 | Object.FindObjectsByType + AssetDatabase.FindAssets/GetDependencies |
| 批改 | 写入,需要 Undo 注册 | ctx.RegisterObjectModification + EditorSceneManager.MarkSceneDirty / AssetDatabase.SaveAssets |
| 生成 | 创建新对象/资产 | ctx.RegisterObjectCreation + AssetDatabase.CreateAsset / PrefabUtility.SaveAsPrefabAsset |
每一类都有共通的"定型"——查询模式必返回 { count, items[] } 便于 AI 续接;批改模式必接 Undo + 标脏;生成模式必走 AssetDatabase 注册以便引用。
与专用工具组合的对比
如果不用 execute_code,场景 4(统计三角形)需要:列举所有 GameObject → 对每个查 MeshFilter 组件 → 读 sharedMesh → 计算 triangles。即使有专用 list_components 工具,也至少 3-N 次 round-trip,N 等于场景对象数。execute_code 一次调用搞定。
场景 6(orphan prefab)则根本无法用专用工具完成——AssetDatabase.GetDependencies 没有现成的 MCP 工具暴露,唯一出路是 execute_code。
写在最后
Editor 自动化的本质从来不是"省下点几下鼠标",而是把人工不擅长的事——大批量遍历、跨资产追溯、严格幂等——交给可重放的代码。execute_code 把这件事的成本压到了"写 30 行 C#"的级别,且无需为每个场景预先定义新工具、无需写 .cs 文件、无需触发 domain reload。
完整工具实现在 FunplayAI/funplay-unity-mcp,开源 MIT 协议。本文所有示例代码都可以直接通过 execute_code MCP 工具调用,或粘进 Editor 菜单脚本独立运行。
如果你也在为 Unity 项目接入 AI 工作流并尝试用一个工具承接更多需求,欢迎仓库提 issue 或讨论。
更多推荐
所有评论(0)