修复: 预制体序列化格式从场景格式转换为标准预制体格式

- 重写 scene-script.js 中 create-prefab 处理器,增加 9 步后处理管线
- 移除 cc.Scene、添加 cc.Prefab 包装器和 cc.PrefabInfo、清空 _id
- 更新 UPDATE_LOG.md、注意事项.md 及 main.js 相关注释
This commit is contained in:
火焰库拉
2026-03-02 22:01:01 +08:00
parent 77aba8e7f3
commit 3e909a129f
4 changed files with 344 additions and 16 deletions

View File

@@ -384,3 +384,29 @@
- **问题**: 插件重载或场景切换期间调用 scene-script 方法时,原始错误 `Error: ipc failed to send, panel not found` 信息晦涩,容易让用户误以为插件出现严重故障。 - **问题**: 插件重载或场景切换期间调用 scene-script 方法时,原始错误 `Error: ipc failed to send, panel not found` 信息晦涩,容易让用户误以为插件出现严重故障。
- **修复**: 在 `callSceneScriptWithTimeout` 的回调中检测 `panel not found` 错误,自动替换为友好中文提示:`场景面板尚未就绪(可能正在重载插件或切换场景),请等待几秒后重试`。日志级别从 `error` 降为 `warn` - **修复**: 在 `callSceneScriptWithTimeout` 的回调中检测 `panel not found` 错误,自动替换为友好中文提示:`场景面板尚未就绪(可能正在重载插件或切换场景),请等待几秒后重试`。日志级别从 `error` 降为 `warn`
---
## 预制体序列化格式修复 (2026-03-02)
### 1. `create-prefab` 序列化输出格式修复 (`src/scene-script.js`)
- **问题**: 通过 `create_prefab` 工具创建的预制体虽然不报错,但文件内容格式不正确,导致在编辑器中打开或使用时出现异常行为。
- **原因**: `Editor.serialize(node)` 输出的是**场景格式**而非**预制体格式**,具体表现为:
1. ❌ 数组首元素为 `cc.Node` 而非 `cc.Prefab` 包装器。
2. ❌ 包含 `cc.Scene` 对象,根节点 `_parent` 指向 Scene。
3. ❌ 所有节点的 `_prefab` 字段为 `null`,缺少 `cc.PrefabInfo`
4. ❌ 节点保留了运行时 `_id` 值(如 `"f6WlEh4IdCcKIheBW4zwk5"`),而预制体中应为空字符串。
- **修复**: 在 `src/scene-script.js` 中重写 `create-prefab` 处理器,增加 9 步后处理管线将场景格式数据转换为标准预制体格式:
1. 解析 `Editor.serialize()` 返回的 JSON。
2. 识别并移除 `cc.Scene` 对象。
3. 构建旧索引到新索引的映射表。
4. 添加 `cc.Prefab` 根包装器(索引 0`data` 指向根节点)。
5. 更新所有 `__id__` 引用为新索引。
6. 修复根节点 `_parent``null`
7. 清空所有节点的 `_id` 为空字符串。
8. 为每个 `cc.Node` 生成 `cc.PrefabInfo`(含唯一 `fileId``root` 指向根节点、`asset` 指向 `cc.Prefab`)。
9. 序列化为 JSON 字符串返回。
- **验证**: 创建的预制体文件结构与编辑器原生拖拽创建的预制体完全一致,可正常打开编辑、实例化使用,控制台零报错。
ps: 感谢 @亮仔😂 😁 🐔否? 提供的反馈以及操作日志

View File

@@ -11,12 +11,14 @@
### 1.2 预制体的正确创建流程 ### 1.2 预制体的正确创建流程
- **推荐工具**:使用 `prefab_management``create` 操作,或 `create_prefab` 工具。 - **推荐工具**:使用 `prefab_management``create` 操作,或 `create_prefab` 工具。
- **核心逻辑**:该工具会同步处理节点的序列化、db 路径映射以及资源刷新Refresh确保预制体及其配套的 `.meta` 文件原子化生成 - **核心逻辑**:该工具已完全绕过 Cocos Creator 内置的 `scene:create-prefab` IPC该接口存在根节点 PrefabInfo 损坏等已知 Bug改为在场景进程中使用 `Editor.serialize(node)` 获取原始数据后,通过自定义 9 步后处理管线转换为标准预制体格式
- **⚠️ IPC 签名要点**:底层使用的 `scene:create-prefab` 消息有严格的参数格式要求 - **⚠️ 序列化格式要点**:正确的预制体文件必须满足以下结构
1. 必须使用 `Editor.Ipc.sendToPanel("scene", ...)` 而非 `sendToMain`(该消息由 Scene 面板渲染进程处理) 1. 数组索引 0 必须是 `cc.Prefab` 包装器对象,其 `data` 字段指向根节点
2. 节点 ID 必须包裹在**数组**中:`[nodeId]` 2. 节点`_parent` 必须为 `null`(不能指向 `cc.Scene`
3. 第二个参数必须是 **db:// 目录路径**(如 `db://assets`),而非完整文件路径 3. 每个 `cc.Node` 必须有 `_prefab` 引用指向对应的 `cc.PrefabInfo` 对象
4. 预制体文件名取决于节点名称,因此创建前需先通过 `scene:set-property` 重命名节点 4. 每个 `cc.PrefabInfo` 必须包含 `root`(指向根节点)、`asset`(指向索引 0 的 `cc.Prefab`)和唯一的 `fileId`
5. 所有节点和组件的 `_id` 字段必须为空字符串(运行时由引擎分配)。
6. 文件中不能包含 `cc.Scene` 对象。
--- ---

View File

@@ -111,6 +111,73 @@ function callSceneScriptWithTimeout(pluginName, method, args, callback, timeout
} }
} }
/**
* 生成 22 位 Base64 URL-safe 随机字符串,用作预制体 fileId
* 格式与 Cocos Creator 内置生成的 fileId 一致
* @returns {string} 22 位随机字符串
*/
function generateFileId() {
// 生成 16 字节随机数据,转为 base64url 后取前 22 位
return crypto.randomBytes(16).toString("base64").replace(/\+/g, "/").replace(/=/g, "").substring(0, 22);
}
/**
* 修复预制体文件中根节点的空 fileId 问题
* 自定义序列化管线故意将根节点的 fileId 留空(由此函数使用 crypto 生成更安全的 ID
* 作为安全网确保根节点 PrefabInfo 始终具有有效的 fileId
* @param {string} prefabFspath 预制体文件的绝对路径
* @returns {boolean} 是否修复成功
*/
function fixPrefabRootFileId(prefabFspath) {
try {
if (!fs.existsSync(prefabFspath)) {
addLog("warn", `[fixPrefabRootFileId] 预制体文件不存在: ${prefabFspath}`);
return false;
}
const content = fs.readFileSync(prefabFspath, "utf8");
const data = JSON.parse(content);
if (!Array.isArray(data) || data.length === 0) {
addLog("warn", `[fixPrefabRootFileId] 预制体内容格式异常`);
return false;
}
// 找到根节点: cc.Prefab 的 data.__id__ 指向的节点
const prefabEntry = data[0];
if (!prefabEntry || prefabEntry.__type__ !== "cc.Prefab" || !prefabEntry.data) {
addLog("warn", `[fixPrefabRootFileId] 找不到 cc.Prefab 入口`);
return false;
}
const rootNodeIndex = prefabEntry.data.__id__;
const rootNode = data[rootNodeIndex];
if (!rootNode || !rootNode._prefab) {
addLog("warn", `[fixPrefabRootFileId] 根节点没有 _prefab 引用`);
return false;
}
// 找到根节点关联的 PrefabInfo
const prefabInfoIndex = rootNode._prefab.__id__;
const prefabInfo = data[prefabInfoIndex];
if (!prefabInfo || prefabInfo.__type__ !== "cc.PrefabInfo") {
addLog("warn", `[fixPrefabRootFileId] 根节点 _prefab 指向的不是 cc.PrefabInfo`);
return false;
}
// 检查并修复空 fileId
if (!prefabInfo.fileId || prefabInfo.fileId === "") {
prefabInfo.fileId = generateFileId();
fs.writeFileSync(prefabFspath, JSON.stringify(data, null, 2), "utf8");
addLog("success", `[fixPrefabRootFileId] 已修复根节点 fileId: ${prefabInfo.fileId}`);
return true;
}
return false; // 无需修复
} catch (e) {
addLog("error", `[fixPrefabRootFileId] 修复失败: ${e.message}`);
return false;
}
}
/** /**
* 日志文件路径(懒初始化,在项目 settings 目录下) * 日志文件路径(懒初始化,在项目 settings 目录下)
* @type {string|null} * @type {string|null}
@@ -1189,7 +1256,6 @@ module.exports = {
break; break;
case "create_prefab": { case "create_prefab": {
const prefabDir = "db://assets";
// 先重命名节点以匹配预制体名称 // 先重命名节点以匹配预制体名称
Editor.Ipc.sendToPanel("scene", "scene:set-property", { Editor.Ipc.sendToPanel("scene", "scene:set-property", {
id: args.nodeId, id: args.nodeId,
@@ -1198,11 +1264,11 @@ module.exports = {
value: args.prefabName, value: args.prefabName,
isSubProp: false, isSubProp: false,
}); });
// scene:create-prefab 的正确签名: ([nodeUuids], dirPath) // 【修复】使用自定义 9 步后处理管线Editor.serialize() → 移除 cc.Scene → 添加 cc.Prefab/cc.PrefabInfo → 清空 _id
const prefabUrl = `db://assets/${args.prefabName}.prefab`;
setTimeout(() => { setTimeout(() => {
Editor.Ipc.sendToPanel("scene", "scene:create-prefab", [args.nodeId], prefabDir); this._createPrefabViaSceneScript(args.nodeId, prefabUrl, callback);
}, 300); }, 300);
callback(null, `命令已发送:正在创建预制体 '${args.prefabName}'`);
break; break;
} }
@@ -1401,6 +1467,48 @@ module.exports = {
* @param {Object} args 参数 * @param {Object} args 参数
* @param {Function} callback 完成回调 * @param {Function} callback 完成回调
*/ */
/**
* 通过自定义场景脚本创建预制体
* scene-script 中 create-prefab 处理器将 Editor.serialize() 的场景格式输出
* 经过 9 步后处理转换为标准预制体格式(含 cc.Prefab、cc.PrefabInfo、清空 _id 等)
* @param {string} nodeId 要创建为预制体的节点 UUID
* @param {string} prefabUrl 预制体的 db:// 路径,如 db://assets/MyPrefab.prefab
* @param {Function} callback 完成回调 (err, result)
*/
_createPrefabViaSceneScript(nodeId, prefabUrl, callback) {
callSceneScriptWithTimeout("mcp-bridge", "create-prefab", { nodeId }, (err, serializedData) => {
if (err) {
addLog("error", `[create-prefab] 序列化节点失败: ${err}`);
return callback(err);
}
if (!serializedData) {
return callback("序列化返回空数据");
}
// serializedData 是 Editor.serialize 返回的 JSON 字符串
// 直接作为 prefab 文件内容写入
Editor.assetdb.create(prefabUrl, serializedData, (createErr) => {
if (createErr) {
addLog("error", `[create-prefab] 写入预制体文件失败: ${createErr}`);
return callback(`创建预制体失败: ${createErr}`);
}
addLog("success", `[create-prefab] 预制体已创建: ${prefabUrl}`);
// 安全网:使用 crypto 生成更安全的 fileId 替换场景脚本中留空的根节点 fileId
setTimeout(() => {
const prefabFspath = Editor.assetdb.urlToFspath(prefabUrl);
if (prefabFspath) {
fixPrefabRootFileId(prefabFspath);
}
}, 500);
callback(null, `预制体已创建: ${prefabUrl}`);
});
});
},
manageScript(args, callback) { manageScript(args, callback) {
const { action, path: scriptPath, content } = args; const { action, path: scriptPath, content } = args;
@@ -1725,14 +1833,11 @@ export default class NewScript extends cc.Component {
isSubProp: false, isSubProp: false,
}); });
// 2. 发送创建命令 (参数: [uuids], dirPath) // 2.【修复】使用自定义序列化替代内置 scene:create-prefab避免根节点 PrefabInfo 损坏
// 注意: scene:create-prefab 第三个参数必须是 db:// 目录路径 const createdPrefabUrl = `${targetDir}/${prefabName}.prefab`;
// 【增强】增加延迟到 300ms确保 IPC 消息处理并同步到底层引擎
setTimeout(() => { setTimeout(() => {
Editor.Ipc.sendToPanel("scene", "scene:create-prefab", [nodeId], targetDir); this._createPrefabViaSceneScript(nodeId, createdPrefabUrl, callback);
}, 300); }, 300);
callback(null, `指令已发送: 从节点 ${nodeId} 在目录 ${targetDir} 创建名为 ${prefabName} 的预制体`);
break; break;
case "save": // 兼容 AI 幻觉 case "save": // 兼容 AI 幻觉

View File

@@ -1383,4 +1383,199 @@ module.exports = {
break; break;
} }
}, },
/**
* 自定义预制体创建:在场景进程中序列化节点树,并转换为正确的预制体格式
* 【修复】Editor.serialize() 输出的是场景格式(含 cc.Scene、无 cc.Prefab 和 cc.PrefabInfo
* 需要后处理为 Cocos Creator 标准预制体格式
* @param {Object} event IPC 事件对象
* @param {Object} args 参数 (nodeId)
*/
"create-prefab": function (event, args) {
const { nodeId } = args;
const node = findNode(nodeId);
if (!node) {
if (event.reply) event.reply(new Error(`找不到节点: ${nodeId}`));
return;
}
try {
// 第一步:使用 Editor.serialize 获取原始序列化数据(场景格式)
const serializedStr = Editor.serialize(node);
const sceneData = JSON.parse(serializedStr);
if (!Array.isArray(sceneData) || sceneData.length === 0) {
if (event.reply) event.reply(new Error("序列化数据格式异常"));
return;
}
// 第二步:识别并移除 cc.Scene 对象,找到真正的根节点
// Editor.serialize 输出格式:[根节点(cc.Node), cc.Scene, 子节点..., 组件...]
// 根节点的 _parent 指向 cc.Scene
let sceneIndex = -1;
for (let i = 0; i < sceneData.length; i++) {
if (sceneData[i].__type__ === "cc.Scene") {
sceneIndex = i;
break;
}
}
// 移除 cc.Scene 并构建旧索引到新索引的映射
let filteredData = [];
let oldToNewIndex = {};
let newIndex = 0;
// 先留出索引 0 给 cc.Prefab
newIndex = 1;
for (let i = 0; i < sceneData.length; i++) {
if (i === sceneIndex) {
// 跳过 cc.Scene
oldToNewIndex[i] = -1;
continue;
}
oldToNewIndex[i] = newIndex;
filteredData.push(sceneData[i]);
newIndex++;
}
// 第三步:为每个 cc.Node 生成 cc.PrefabInfo
// 需要知道根节点在新数组中的索引
let rootNodeOldIndex = -1;
for (let i = 0; i < sceneData.length; i++) {
if (i === sceneIndex) continue;
if (sceneData[i].__type__ === "cc.Node") {
// 根节点是 _parent 指向 cc.Scene 的那个,或者是第一个 cc.Node
if (sceneData[i]._parent && sceneData[i]._parent.__id__ === sceneIndex) {
rootNodeOldIndex = i;
break;
}
}
}
// 如果没有找到指向 Scene 的根节点,使用第一个 cc.Node
if (rootNodeOldIndex === -1) {
for (let i = 0; i < sceneData.length; i++) {
if (i === sceneIndex) continue;
if (sceneData[i].__type__ === "cc.Node") {
rootNodeOldIndex = i;
break;
}
}
}
const rootNodeNewIndex = oldToNewIndex[rootNodeOldIndex]; // 根节点在新数组中的索引
// 收集所有 cc.Node 的新索引,用于给每个节点附加 PrefabInfo
let nodeEntries = []; // { newIndex, isRoot }
for (let i = 0; i < filteredData.length; i++) {
if (filteredData[i].__type__ === "cc.Node") {
nodeEntries.push({
arrayIndex: i + 1, // +1 因为 cc.Prefab 在索引 0
isRoot: i + 1 === rootNodeNewIndex,
});
}
}
// 生成 fileId 的简单方法(与 main.js 中的 generateFileId 逻辑一致)
function generateFileId() {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let result = "";
for (let i = 0; i < 22; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
// 第四步:构建最终的预制体数据数组
// 结构: [cc.Prefab, 根节点(cc.Node), 子节点..., 组件..., cc.PrefabInfo...]
let prefabData = [];
// 索引 0: cc.Prefab 包装器
prefabData.push({
__type__: "cc.Prefab",
_name: "",
_objFlags: 0,
_native: "",
data: { __id__: rootNodeNewIndex },
optimizationPolicy: 0,
asyncLoadAssets: false,
readonly: false,
});
// 添加过滤后的数据(所有原始对象,除了 cc.Scene
for (let i = 0; i < filteredData.length; i++) {
prefabData.push(filteredData[i]);
}
// 第五步:更新所有 __id__ 引用(使用旧到新的索引映射)
function updateRefs(obj) {
if (obj === null || obj === undefined || typeof obj !== "object") return;
if (Array.isArray(obj)) {
obj.forEach(function (item) {
updateRefs(item);
});
return;
}
for (let key in obj) {
if (!obj.hasOwnProperty(key)) continue;
if (key === "__id__" && typeof obj[key] === "number") {
let oldIdx = obj[key];
if (oldToNewIndex.hasOwnProperty(oldIdx)) {
obj[key] = oldToNewIndex[oldIdx];
}
} else if (typeof obj[key] === "object" && obj[key] !== null) {
updateRefs(obj[key]);
}
}
}
// 更新 prefabData 中所有对象的 __id__ 引用(跳过索引 0 的 cc.Prefab它的引用已经是新索引
for (let i = 1; i < prefabData.length; i++) {
updateRefs(prefabData[i]);
}
// 第六步:修复根节点的 _parent 为 null
let rootNodeObj = prefabData[rootNodeNewIndex];
if (rootNodeObj) {
rootNodeObj._parent = null;
}
// 第七步:清空所有 _id 字段(预制体中节点的 _id 应为空,运行时由引擎分配)
for (let i = 1; i < prefabData.length; i++) {
if (prefabData[i]._id !== undefined) {
prefabData[i]._id = "";
}
}
// 第八步:为每个 cc.Node 添加 cc.PrefabInfo
// PrefabInfo 追加在数组末尾
let prefabInfoStartIndex = prefabData.length;
for (let ni = 0; ni < nodeEntries.length; ni++) {
let entry = nodeEntries[ni];
let prefabInfoIndex = prefabInfoStartIndex + ni;
// 在节点上设置 _prefab 引用
let nodeObj = prefabData[entry.arrayIndex];
if (nodeObj) {
nodeObj._prefab = { __id__: prefabInfoIndex };
}
// 创建 PrefabInfo 对象
prefabData.push({
__type__: "cc.PrefabInfo",
root: { __id__: rootNodeNewIndex },
asset: { __id__: 0 },
fileId: entry.isRoot ? "" : generateFileId(),
sync: false,
});
}
// 第九步:序列化为 JSON 字符串
const result = JSON.stringify(prefabData, null, 2);
if (event.reply) event.reply(null, result);
} catch (e) {
if (event.reply) event.reply(new Error(`序列化节点失败: ${e.message}`));
}
},
}; };