name: vrchat-ma-blendshape-sync-fix description: 修复 ModularAvatarBlendshapeSync 组件的 MA-1005 警告(referencePath / Blendshape 字段错填),含 Unity MCP 反射批量修复脚本和 Cazalis 服装包已知 bug
VRChat ModularAvatar BlendshapeSync 修复
适用场景
Unity Console 出现 MA-1005 警告:
最常见原因(按发生率排序): 1. referencePath 错填(指向不存在的节点,例 Cazalis 服装包写成 "Kikyo_Body_Base") 2. Blendshape 名字大小写错误(例 "Breasts_big" 实际是 "Breasts_Big") 3. 目标节点已被删除/重命名 4. AvatarObjectReference 缓存字段过期(path 已改但 Get() 返回旧值)
已知 bug 名单
Cazalis 服装包(zigai 系列三件套)
| 部件 | 错填 referencePath | Blendshape 大小写错误 |
|---|---|---|
| 01_Black/Bag | Kikyo_Body_Base | Breasts_big / Breasts_small |
| 01_Black/Blouse | Kikyo_Body_Base | Breasts_big / Breasts_small |
| 01_Black/Knit | Kikyo_Body_Base | Breasts_big / Breasts_small |
每个组件 2 个 binding,每个 Avatar 6 个 binding。修复方式:
- referencePath: Kikyo_Body_Base → Body_Base
- Blendshape: Breasts_big/small → Breasts_Big/Small
ferencePath: Kikyo_Body_Base → Body_Base
- Blendshape: Breasts_big/small → Breasts_Big/Small## 数据结构关键点(容易踩坑)
// 组件: ModularAvatarBlendshapeSync
// Bindings: List<BlendshapeBinding> ← struct,值类型!
public struct BlendshapeBinding {
public AvatarObjectReference ReferenceMesh; // class(引用类型)
public string Blendshape; // 源 blendshape 名
public string LocalBlendshape; // 目标 blendshape 名(空=同名)
}
public class AvatarObjectReference {
public string referencePath;
// 内部缓存(私有字段,要清掉)
private bool _cacheValid;
private string _cachedPath;
private Object _cachedReference;
}
坑 1:BlendshapeBinding 是 struct,从 list 取出后修改不写回会丢失:
// ❌ 错的
bindings[i].Blendshape = "Breasts_Big"; // 编译报错或修改的是临时副本
// ✅ 对的
var b = bindings[i];
b.GetType().GetField("Blendshape").SetValue(b, "Breasts_Big");
bindings[i] = b; // 必须写回
坑 2:AvatarObjectReference 是 class,可以直接修改字段,但要清缓存:
refMesh.referencePath = "Body_Base";
// 还要清 _cacheValid / _cachedPath / _cachedReference 否则 Get() 仍返回旧值
rmt.GetField("_cacheValid").SetValue(refMesh, false);
rmt.GetField("_cachedPath").SetValue(refMesh, "");
rmt.GetField("_cachedReference").SetValue(refMesh, null);
批量修复脚本(Unity MCP execute_code)
完整脚本:见 scripts/fix-ma-blendshape-sync.cs
核心流程: 1. 遍历指定 Avatar 列表的所有子 Transform 2. 找所有 ModularAvatarBlendshapeSync 组件 3. 反射读取 Bindings,逐个检查 referencePath / Blendshape 4. 匹配已知错误模式时修正,必须 bindings[i] = binding 写回 5. 清 AvatarObjectReference 缓存 6. EditorUtility.SetDirty + SaveScene ,必须 bindings[i] = binding 写回 5. 清 AvatarObjectReference 缓存 6. EditorUtility.SetDirty + SaveScene## 验证步骤(极其重要!)
⚠️ 2026-05-31 教训:Get() 假阳性陷阱
AvatarObjectReference 序列化的真实字段不止 referencePath 一个,至少有 7 个:
- referencePath (public string)
- targetObject (private GameObject) ← 关键!
- ReferencesLockedAtFrame (long)
- _cacheSeq (private)
- _cacheValid (private)
- _cachedPath (private)
- _cachedReference (private)
修复时只改 referencePath 会出现:
- ✅ Get() 调用动态解析新路径,返回正确 GameObject(看起来修好了)
- ❌ .unity 序列化时仍写旧的 targetObject 引用
- ❌ Avatar 被复制 / Prefab 重生成后,新 Avatar 的 targetObject 仍指向源 Avatar 的 GameObject,跨 Avatar 引用 → MA-1005 仍报
完整修复必须同时改两个字段:
// 方法 A(推荐):调用官方 Set 方法
refMesh.Set(targetGameObject); // 内部会同时设置 path + targetObject + IsConsistent 校验
// 方法 B:反射裸改(必须三件套)
refMesh.referencePath = "Body_Base";
var targetField = typeof(AvatarObjectReference)
.GetField("targetObject", BindingFlags.Instance | BindingFlags.NonPublic);
targetField.SetValue(refMesh, targetGameObject);
// 然后清缓存
typeof(AvatarObjectReference)
.GetField("_cacheValid", BindingFlags.Instance | BindingFlags.NonPublic)
.SetValue(refMesh, false);
反射读 private 字段必须用 NonPublic BindingFlags(否则 GetField 返回 null):
// ❌ 默认只拿 public,private targetObject 返回 null
var f = type.GetField("targetObject");
// ✅ 必须显式
var f = type.GetField("targetObject",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
修复后三重验证: bject", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
修复后**三重验证**:```csharp
// 1. 扫描确认所有 binding referencePath / Blendshape 都不在错误名单
// 2. Get() 能 resolve 到 GameObject
var resolved = refMesh.Get(component);
// 3. ★必须★ 反射读 targetObject 字段,确认指向当前 Avatar 而非源 Avatar
var targetField = typeof(AvatarObjectReference)
.GetField("targetObject", BindingFlags.Instance | BindingFlags.NonPublic);
var t = targetField.GetValue(refMesh) as GameObject;
// t != null && t.transform.root == component.transform.root 才算成功
⚠️ fake null 陷阱:UnityEngine.Object 被销毁后 == null 是 true 但反射 GetValue 后强转 as GameObject 可能拿到 destroyed 引用,访问 .name / .transform 会抛 NullReferenceException。先 == null 检查再访问。
转 as GameObject 可能拿到 destroyed 引用,访问 .name / .transform 会抛 NullReferenceException。先 == null 检查再访问。## 安全规范(参照改模硬纪律)
- 修复前必备份:
AssetDatabase.CopyAsset("Assets/Scene.unity", "Assets/_Backup_xxx/Scene.unity.bak") - 样本先行:先修 1 个 binding,用户验证 OK 再批量
- 修 zigai02 等"原版"对象的破例条件:纯字符串字段 + 可逆 + 修复 bug 而非新增风险
- 修复完整可逆:保留 .bak 文件至少一周
Cazalis 案例完整执行流(2026-05-31)
| Phase | 范围 | 数量 | 验证方式 |
|---|---|---|---|
| 0 | 备份场景 | 1 file (8.5MB) | size 校验 |
| 1 | 修 1 样本 zigai02/01_Black/Bag | 2 binding | Get() resolved != null |
| 2 | 批量修剩余 | 16 binding | 扫描确认 0 残留 + Get() 54/54 resolved |
| 3 | ⚠️ 复制 Avatar 后用户报 ZG_02 仍报错 | — | targetObject 指向源 Avatar |
最终:8 components / 18 bindings 全部修复,0 MA-1005 警告。
⚠️ 助手翻车记录与教训(2026-05-31)
事件复盘: 1. 助手 Phase 2 报告"54/54 全部 resolved"是假阳性 — 只验 Get() 没验 targetObject 字段 2. 用户拆分 Avatar 后 ZG_01/ZG_02 仍报 MA-1005,因为复制时拷贝了源 Avatar 的 targetObject 引用 3. 助手第二次扫描以为"all OK",实际是反射读 private 字段没加 BindingFlags.NonPublic 全返 null 走 EX 分支 4. 用户三次回过头说"你只换了源没换目标"才意识到 targetObject 是独立字段
根本教训: - ❗ 修复任何 Unity Component 字段前,必须先用 SerializedObject 看完整字段列表,不要假设 "看 Inspector 上的字段就够了" - ❗ Get() / 静态 API 解析正常 ≠ 序列化正确。验证一定要看 .unity 文件实际写了什么(或反射读所有可序列化字段) - ❗ 修 MA 字段优先用官方 Set/Clone/IsConsistent API,反射裸改属于最后手段 - ❗ 当 user 报错与 verify 结果矛盾时,立即怀疑 verify 而非 user
常见问题:复制 Avatar 后上传被覆盖
原因
从已有 Avatar 复制出来的新 Avatar,PipelineManager 组件会保留原 Avatar 的 Blueprint ID(avtr_xxx-xxxx),VRChat SDK 会认为是同一个 Avatar 的新版本,后上传覆盖前面的。
eManager组件会保留原 Avatar 的 **Blueprint ID**(avtr_xxx-xxxx),VRChat SDK 会认为是同一个 Avatar 的新版本,后上传覆盖前面的。### 解决方法(每个新 Avatar 操作一次)
1. 选中 Avatar 根节点 → 找到Pipeline Manager` 组件
2. 点击 Blueprint ID 右侧 Detach (Optional) 按钮,ID 变空白
3. VRCSDK 控制面板 → Builder → 确认 Avatar 名称正确 → Build & Publish
4. 第一次上传勾选 New Avatar,生成新的独立 Blueprint ID
⚠️ 原 Avatar 不要 detach,保留原 ID 保持原有上传记录。