diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ee8d72a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +# 1.0.1 / 2023-09-26 + +**调整** + +- 行为树编辑器面板默认在`Cocos`主编辑器内打开(场景 Panel 旁边),更方便用户操作。 +- 切换`层级管理器`节点时,会自动切换为当前节点的行为树,如果当前节点没有行为树,则显示 mask 提示用户`添加组件`并`创建json`。 +- 修改`BehaviorEditor`组件按钮文案为`Create / Editor`。 +- 在已经打开行为树编辑器的情况下,再次点击`BehaviorEditor`组件的`Create / Editor`,可以起到重启插件的效果,在更新了外部资源(JSON,组件)。 +- 画布大小从`2400 1600`下降为` 2000 1400`。 +- 禁止设置非组合节点`Composite`的`Abort Type`属性。 +- 左下角增加`Root`按钮回到根节点处 +- 样式修改 + + # 1.0.0 / 2023-09-21 + + **首次发布** diff --git a/dist/contributions/inspector/behavior-editor.js b/dist/contributions/inspector/behavior-editor.js index 7739feb..eb2e1e4 100644 --- a/dist/contributions/inspector/behavior-editor.js +++ b/dist/contributions/inspector/behavior-editor.js @@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.ready = exports.update = exports.$ = exports.template = void 0; exports.template = ` -
- Edit Tree +
+ Create / Edit
`; @@ -64,15 +64,17 @@ function ready() { }); if (success) { console.log(`JSON文件挂载成功`); - Editor.Message.request("behavior-eden", "open-panel"); } else { console.warn("JSON文件挂载失败"); + return; } } - else { - // 打开插件面板 - Editor.Message.request("behavior-eden", "open-panel"); + // 打开插件面板 + const success = await Editor.Message.request("behavior-eden", "open-panel"); + // success为false,可能是已经打开了,通知面板刷新 + if (!success) { + await Editor.Message.request("behavior-eden", "refresh-panel"); } }); } diff --git a/dist/main.js b/dist/main.js index 05c2c41..240bc72 100644 --- a/dist/main.js +++ b/dist/main.js @@ -11,8 +11,13 @@ const package_json_1 = __importDefault(require("../package.json")); * @zh 为扩展的主进程的注册方法 */ exports.methods = { - openPanel() { - Editor.Panel.open(package_json_1.default.name); + async openPanel() { + return await Editor.Panel.openBeside("scene", package_json_1.default.name); + }, + async refreshPanel() { + await Editor.Panel.close(package_json_1.default.name); + await new Promise((rs) => setTimeout(rs, 300)); + await Editor.Panel.openBeside("scene", package_json_1.default.name); }, }; /** diff --git a/dist/panels/default/index.js b/dist/panels/default/index.js index 5f7ba67..faac742 100644 --- a/dist/panels/default/index.js +++ b/dist/panels/default/index.js @@ -10,8 +10,8 @@ const node_1 = require("../../runtime/node"); const decorator_1 = require("../../runtime/core/decorator"); const utils_1 = require("../../runtime/core/utils"); // 画布宽高 -const CANVAS_WIDTH = 2400; -const CANVAS_HEIGHT = 1600; +const CANVAS_WIDTH = 2000; +const CANVAS_HEIGHT = 1400; // 节点box属性 const BOX_WIDTH = 100; // const BOX_HEIGHT = 64; //height通过getBoxHeight方法动态计算 @@ -141,6 +141,21 @@ const ParentNodes = [...decorator_1.nodeClsMap.values()] return false; }) .map((cls) => cls.name); +/*** + * 获取所有Composite类型的节点 + */ +const CompositeNodes = [...decorator_1.nodeClsMap.values()] + .filter((cls) => { + let parentCls = getParentCls(cls); + while (parentCls) { + if (parentCls.name === node_1.Composite.name) { + return true; + } + parentCls = getParentCls(parentCls); + } + return false; +}) + .map((cls) => cls.name); const activeCursor = () => { stage && (stage.container().style.cursor = "pointer"); }; @@ -151,7 +166,7 @@ const component = lib_1.Vue.extend({ template: lib_1.fs.readFileSync(path_1.default.join(__dirname, "../../../src/panels/static/template/vue/app.html"), "utf-8"), $: { tree: "#tree", - left: "#left", + scroll: "#scroll", }, filters: { toUpperCase(value) { @@ -189,6 +204,7 @@ const component = lib_1.Vue.extend({ */ nodeCompMethodInfo: {}, logs: [], + maskText: "", }; }, computed: { @@ -253,12 +269,143 @@ const component = lib_1.Vue.extend({ }, }, async mounted() { - this.initCanvas(); - await this.getAssets(); - await this.selectCurrentAsset(); - this.$refs.left.scroll((CANVAS_WIDTH - this.$refs.left.getBoundingClientRect().width) / 2, ROOT_Y - 50); + this.init(); }, methods: { + /*** + * 初始化相关 + */ + async init() { + // 初始化画布 + this.initCanvas(); + // 根据当前选中节点初始化行为树 + await this.initSelection(); + this.backRoot(); + }, + initCanvas() { + // 初始化舞台 + stage = new lib_1.Konva.Stage({ + container: this.$refs.tree, + width: CANVAS_WIDTH, + height: CANVAS_HEIGHT, + }); + // 基础层,目前只有一层,每层都会生成一个canvas + baseLayer = new lib_1.Konva.Layer(); + // 背景组 + bgGroup = this.generateBg(); + baseLayer.add(bgGroup); + // 静态箭头层 + staticArrowGroup = new lib_1.Konva.Group(); + baseLayer.add(staticArrowGroup); + // 动态箭头层(用户手动拉出来的箭头) + dynamicArrowGroup = this.generateDynamicArrowGroup(); + baseLayer.add(dynamicArrowGroup); + // Root组 + rootGroup = this.generateRoot(); + baseLayer.add(rootGroup); + // 节点层 + nodeGroup = new lib_1.Konva.Group(); + baseLayer.add(nodeGroup); + // 框选层 + selectionGroup = this.generatesSelectionGroup(); + baseLayer.add(selectionGroup); + // 层添加到舞台 + stage.add(baseLayer); + }, + // 获取json文件 + async initAssets() { + const behaviorTreeComponentUuids = []; + const dfs = (node) => { + if (!node) { + return; + } + for (const comp of node.components) { + if (comp.type === BTreeCompName) { + behaviorTreeComponentUuids.push(comp.value); + break; + } + } + for (const item of node.children) { + dfs(item); + } + }; + // 获取场景节点树 + const sceneNode = await Editor.Message.request("scene", "query-node-tree"); + // 收集场景上所有BehaviorTree组件uuid + dfs(sceneNode); + // 收集BehaviorTree组件上的json路径 + const rawUrls = await Promise.all(behaviorTreeComponentUuids.map((uuid) => Editor.Message.request("scene", "execute-component-method", { + uuid: uuid, + name: "getAssetUrl", + }))); + // 过滤空的并去重 + const urls = [...new Set(rawUrls.filter(Boolean))]; + // 根据url获取所有json文件信息 + const assets = await Promise.all(urls.map((url) => Editor.Message.request("asset-db", "query-asset-info", url))); + this.assets = assets.map(({ name, source, file }) => ({ name, url: source, file: file, content: "" })); + }, + async initSelection() { + // 找到当前选中的节点 + const node = await Editor.Message.request("scene", "query-node", Editor.Selection.getSelected("node")); + // 未选中节点或者选中的是场景节点(场景节点不能添加组件) + if (!(node === null || node === void 0 ? void 0 : node.__comps__)) { + this.maskText = "请选中一个非场景根节点非空名称节点来开始行为树制作"; + return; + } + // 找到BehaviorTree组件 + const index = node.__comps__.findIndex((v) => v.type === BTreeCompName); + if (index === -1) { + this.maskText = "要制作行为树,需要先为当前节点添加行为树组件"; + return; + } + const comp = node.__comps__[index]; + // 调用BehaviorTree组件上的方法 + const url = await Editor.Message.request("scene", "execute-component-method", { + uuid: comp.value.uuid.value, + name: "getAssetUrl", + }); + // JSON文件不存在 + if (!url) { + this.maskText = "当前行为树组件缺少JSON资源,点击BehaviorEditor组件按钮即可创建"; + return; + } + this.maskText = ""; + // 选中此文件,设置好currentAsset才能把组件nodes数据同步到JSON + this.handleSelectAsset(url); + }, + async handleSelectAsset(url) { + var _a; + // 实时获取当前场景所有behaviorTree组件上的json文件 + await this.initAssets(); + const json = this.assets.find((e) => e.url === url); + if (!json) { + this.showWarn("JSON文件不存在"); + return; + } + if (((_a = this.currentAsset) === null || _a === void 0 ? void 0 : _a.url) === (json === null || json === void 0 ? void 0 : json.url)) { + return; + } + this.currentAsset = json; + try { + const content = await lib_1.fs.readJSONSync(json.file); + this.currentAsset.content = content; + this.render(); + } + catch (e) { + if (e instanceof SyntaxError) { + // JSON文件语法异常的情况下,初始化内容 + this.showWarn("JSON文件内容初始化成功"); + const content = { nodes: [] }; + await lib_1.fs.writeJSONSync(json.file, content); + this.currentAsset.content = content; + this.render(); + } + else { + // 输出其他异常 + this.showWarn(e); + } + } + }, handlePanelChange(value) { this.currentPanel = value; }, @@ -275,7 +422,7 @@ const component = lib_1.Vue.extend({ this.render(); } }, - async handleEventNodeChange(lifeCycle, uuid = "") { + async handleEventNodeChange(lifeCycle, uuid = "", shouldSave = true) { if (!this.currentNode) { this.showWarn("当前节点不存在"); return; @@ -287,9 +434,13 @@ const component = lib_1.Vue.extend({ else { this.currentNode.event[lifeCycle].comp = ""; this.currentNode.event[lifeCycle].method = ""; - this.currentNode.event[lifeCycle].data = ""; + // 事件参数就不手动清空了 + // this.currentNode.event[lifeCycle].data = ""; + } + // 点击canvas某个节点的时候,会触发handleEventNodeChange获取场景节点数据,此时不保存数据 + if (shouldSave) { + await this.saveAsset(); } - await this.saveAsset(); }, async getNodeCompMethods(lifeCycle, uuid) { const [nodeInfo, compMethodInfo] = await Promise.all([ @@ -331,132 +482,16 @@ const component = lib_1.Vue.extend({ this.currentNode.event[lifeCycle].data = data; await this.saveAsset(); }, - async selectCurrentAsset() { - // 找到点击组件的节点 - const node = await Editor.Message.request("scene", "query-node", Editor.Selection.getSelected("node")); - if (!node) { - this.showWarn(`未选中节点`); - return; - } - // 找到BehaviorTree组件 - const index = node.__comps__.findIndex((v) => v.type === BTreeCompName); - if (index === -1) { - this.showWarn(`节点未挂载【${BTreeCompName}】组件`); - return; - } - const comp = node.__comps__[index]; - // 调用BehaviorTree组件上的方法 - const url = await Editor.Message.request("scene", "execute-component-method", { - uuid: comp.value.uuid.value, - name: "getAssetUrl", - }); - // JSON文件不存在 - if (!url) { - this.showWarn(`【${BTreeCompName}】组件未指定JSON文件`); - return; - } - // 选中此文件,设置好currentAsset才能把组件nodes数据同步到JSON - this.handleSelectAsset(url); - }, - async handleSelectAsset(url) { - const json = this.assets.find((e) => e.url === url); - if (!json) { - this.showWarn("JSON文件不存在"); - return; - } - this.currentAsset = json; - try { - const content = await lib_1.fs.readJSONSync(json.file); - this.currentAsset.content = content; - this.render(); - } - catch (e) { - if (e instanceof SyntaxError) { - // JSON文件语法异常的情况下,初始化内容 - this.showWarn("JSON文件内容初始化成功"); - const content = { nodes: [] }; - await lib_1.fs.writeJSONSync(json.file, content); - this.currentAsset.content = content; - this.render(); - } - else { - // 输出其他异常 - this.showWarn(e); - } - } - }, - // 全部所有behaviorTree组件上使用到的json文件 - async getAssets() { - const behaviorTreeComponentUuids = []; - const dfs = (node) => { - if (!node) { - return; - } - for (const comp of node.components) { - if (comp.type === BTreeCompName) { - behaviorTreeComponentUuids.push(comp.value); - break; - } - } - for (const item of node.children) { - dfs(item); - } - }; - // 获取场景节点树 - const sceneNode = await Editor.Message.request("scene", "query-node-tree"); - // 收集场景上所有BehaviorTree组件uuid - dfs(sceneNode); - // 收集BehaviorTree组件上的json路径 - const rawUrls = await Promise.all(behaviorTreeComponentUuids.map((uuid) => Editor.Message.request("scene", "execute-component-method", { - uuid: uuid, - name: "getAssetUrl", - }))); - // 过滤空的并去重 - const urls = [...new Set(rawUrls.filter(Boolean))]; - // 根据url获取所有json文件信息 - const assets = await Promise.all(urls.map((url) => Editor.Message.request("asset-db", "query-asset-info", url))); - this.assets = assets.map(({ name, source, file }) => ({ name, url: source, file: file, content: "" })); - }, async saveAsset() { var _a; - const url = (_a = this.currentAsset) === null || _a === void 0 ? void 0 : _a.url; - if (!url) { + const file = (_a = this.currentAsset) === null || _a === void 0 ? void 0 : _a.file; + if (!file) { this.showWarn("数据存储失败,未指定JSON"); return; } - const content = JSON.stringify(this.currentAsset.content, null, 2); + const content = this.currentAsset.content; // 修改json文件 - await Editor.Message.request("asset-db", "save-asset", url, content); - }, - initCanvas() { - // 初始化舞台 - stage = new lib_1.Konva.Stage({ - container: this.$refs.tree, - width: CANVAS_WIDTH, - height: CANVAS_HEIGHT, - }); - // 基础层,目前只有一层,每层都会生成一个canvas - baseLayer = new lib_1.Konva.Layer(); - // 背景组 - bgGroup = this.generateBg(); - baseLayer.add(bgGroup); - // 静态箭头层 - staticArrowGroup = new lib_1.Konva.Group(); - baseLayer.add(staticArrowGroup); - // 动态箭头层(用户手动拉出来的箭头) - dynamicArrowGroup = this.generateDynamicArrowGroup(); - baseLayer.add(dynamicArrowGroup); - // Root组 - rootGroup = this.generateRoot(); - baseLayer.add(rootGroup); - // 节点层 - nodeGroup = new lib_1.Konva.Group(); - baseLayer.add(nodeGroup); - // 框选层 - selectionGroup = this.generatesSelectionGroup(); - baseLayer.add(selectionGroup); - // 层添加到舞台 - stage.add(baseLayer); + lib_1.fs.writeJSONSync(file, content); }, /*** * 渲染画布 @@ -568,7 +603,7 @@ const component = lib_1.Vue.extend({ type, abortType: this.AbortType.None, x: (CANVAS_WIDTH - BOX_WIDTH) / 2, - y: CANVAS_HEIGHT / 4, + y: ROOT_Y + 100, isRoot: false, children: [], event: { @@ -828,7 +863,7 @@ const component = lib_1.Vue.extend({ if (e.evt.buttons === 4) { const movementX = e.evt.movementX; const movementY = e.evt.movementY; - this.$refs.left.scrollBy(-movementX, -movementY); + this.$refs.scroll.scrollBy(-movementX, -movementY); return; } if (!selection.visible()) { @@ -981,11 +1016,11 @@ const component = lib_1.Vue.extend({ selectedBoxes = []; this.currentNode = node; this.handlePanelChange(1); - this.render(); // 获取节点事件相关信息 - this.handleEventNodeChange(this.onStart, this.currentNode.event[this.onStart].node); - this.handleEventNodeChange(this.onUpdate, this.currentNode.event[this.onUpdate].node); - this.handleEventNodeChange(this.onEnd, this.currentNode.event[this.onEnd].node); + this.handleEventNodeChange(this.onStart, this.currentNode.event[this.onStart].node, false); + this.handleEventNodeChange(this.onUpdate, this.currentNode.event[this.onUpdate].node, false); + this.handleEventNodeChange(this.onEnd, this.currentNode.event[this.onEnd].node, false); + this.render(); }); return wrapper; }, @@ -1247,6 +1282,12 @@ const component = lib_1.Vue.extend({ } return BOX_HEIGHT; }, + isComposite(node) { + if (!node) { + return false; + } + return CompositeNodes.includes(node.type); + }, showWarn(text) { let time = getTime(); const content = `${time}:${text}`; @@ -1256,6 +1297,11 @@ const component = lib_1.Vue.extend({ this.logs.push({ id: uuid(), content }); console.warn(content); }, + // 视角回到root节点 + backRoot() { + var _a, _b; + (_b = (_a = this.$refs) === null || _a === void 0 ? void 0 : _a.scroll) === null || _b === void 0 ? void 0 : _b.scroll((CANVAS_WIDTH - this.$refs.scroll.getBoundingClientRect().width) / 2, ROOT_Y - 50); + }, }, }); const panelDataMap = new WeakMap(); @@ -1281,7 +1327,14 @@ module.exports = Editor.Panel.define({ data() { return {}; }, - methods: {}, + methods: { + initSelection() { + const vm = panelDataMap.get(this); + if (vm) { + vm.initSelection(); + } + }, + }, ready() { if (this.$.app) { const vm = new component(); diff --git a/dist/runtime/core/Blackboard.js b/dist/runtime/core/Blackboard.js index 3cf8364..f4b72c7 100644 --- a/dist/runtime/core/Blackboard.js +++ b/dist/runtime/core/Blackboard.js @@ -22,5 +22,5 @@ class Blackboard { this.map.clear(); } } -exports.Blackboard = Blackboard; Blackboard.map = new Map(); +exports.Blackboard = Blackboard; diff --git a/package.json b/package.json index 3c1bec3..8c775b8 100644 --- a/package.json +++ b/package.json @@ -37,9 +37,14 @@ "openPanel" ] }, - "send-to-panel": { + "refresh-panel": { "methods": [ - "default.hello" + "refreshPanel" + ] + }, + "selection:select": { + "methods": [ + "default.initSelection" ] } }, diff --git a/src/contributions/inspector/behavior-editor.ts b/src/contributions/inspector/behavior-editor.ts index 6d6b463..cf4066c 100644 --- a/src/contributions/inspector/behavior-editor.ts +++ b/src/contributions/inspector/behavior-editor.ts @@ -3,8 +3,8 @@ type Selector<$> = { $: Record }; export const template = ` -
- Edit Tree +
+ Create / Edit
`; @@ -69,13 +69,16 @@ export function ready(this: Selector & any) { }); if (success) { console.log(`JSON文件挂载成功`); - Editor.Message.request("behavior-eden", "open-panel"); } else { console.warn("JSON文件挂载失败"); + return; } - } else { - // 打开插件面板 - Editor.Message.request("behavior-eden", "open-panel"); + } + // 打开插件面板 + const success = await Editor.Message.request("behavior-eden", "open-panel"); + // success为false,可能是已经打开了,通知面板刷新 + if (!success) { + await Editor.Message.request("behavior-eden", "refresh-panel"); } }); } diff --git a/src/main.ts b/src/main.ts index 4821c49..f736cef 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,8 +6,13 @@ import packageJSON from "../package.json"; * @zh 为扩展的主进程的注册方法 */ export const methods: { [key: string]: (...any: any) => any } = { - openPanel() { - Editor.Panel.open(packageJSON.name); + async openPanel() { + return await Editor.Panel.openBeside("scene", packageJSON.name); + }, + async refreshPanel() { + await Editor.Panel.close(packageJSON.name); + await new Promise((rs) => setTimeout(rs, 300)); + await Editor.Panel.openBeside("scene", packageJSON.name); }, }; diff --git a/src/panels/default/index.ts b/src/panels/default/index.ts index fd07529..cebe2a9 100644 --- a/src/panels/default/index.ts +++ b/src/panels/default/index.ts @@ -6,8 +6,8 @@ import { nodeClsMap } from "../../runtime/core/decorator"; import { buildTree, preOrder } from "../../runtime/core/utils"; // 画布宽高 -const CANVAS_WIDTH = 2400; -const CANVAS_HEIGHT = 1600; +const CANVAS_WIDTH = 2000; +const CANVAS_HEIGHT = 1400; // 节点box属性 const BOX_WIDTH = 100; // const BOX_HEIGHT = 64; //height通过getBoxHeight方法动态计算 @@ -149,6 +149,24 @@ const ParentNodes = [...nodeClsMap.values()] }) .map((cls) => cls.name); +/*** + * 获取所有Composite类型的节点 + */ +const CompositeNodes = [...nodeClsMap.values()] + .filter((cls) => { + let parentCls = getParentCls(cls); + while (parentCls) { + if (parentCls.name === Composite.name) { + return true; + } + + parentCls = getParentCls(parentCls); + } + + return false; + }) + .map((cls) => cls.name); + const activeCursor = () => { stage && (stage.container().style.cursor = "pointer"); }; @@ -160,7 +178,7 @@ const component = Vue.extend({ template: fs.readFileSync(path.join(__dirname, "../../../src/panels/static/template/vue/app.html"), "utf-8"), $: { tree: "#tree", - left: "#left", + scroll: "#scroll", }, filters: { toUpperCase(value) { @@ -199,6 +217,7 @@ const component = Vue.extend({ */ nodeCompMethodInfo: {}, logs: [], + maskText: "", }; }, computed: { @@ -267,12 +286,165 @@ const component = Vue.extend({ }, }, async mounted() { - this.initCanvas(); - await this.getAssets(); - await this.selectCurrentAsset(); - this.$refs.left.scroll((CANVAS_WIDTH - this.$refs.left.getBoundingClientRect().width) / 2, ROOT_Y - 50); + this.init(); }, methods: { + /*** + * 初始化相关 + */ + async init() { + // 初始化画布 + this.initCanvas(); + // 根据当前选中节点初始化行为树 + await this.initSelection(); + this.backRoot(); + }, + initCanvas() { + // 初始化舞台 + stage = new Konva.Stage({ + container: this.$refs.tree, + width: CANVAS_WIDTH, + height: CANVAS_HEIGHT, + }); + // 基础层,目前只有一层,每层都会生成一个canvas + baseLayer = new Konva.Layer(); + + // 背景组 + bgGroup = this.generateBg(); + baseLayer.add(bgGroup); + + // 静态箭头层 + staticArrowGroup = new Konva.Group(); + baseLayer.add(staticArrowGroup); + + // 动态箭头层(用户手动拉出来的箭头) + dynamicArrowGroup = this.generateDynamicArrowGroup(); + baseLayer.add(dynamicArrowGroup); + + // Root组 + rootGroup = this.generateRoot(); + baseLayer.add(rootGroup); + + // 节点层 + nodeGroup = new Konva.Group(); + baseLayer.add(nodeGroup); + + // 框选层 + selectionGroup = this.generatesSelectionGroup(); + baseLayer.add(selectionGroup); + + // 层添加到舞台 + stage.add(baseLayer); + }, + // 获取json文件 + async initAssets() { + const behaviorTreeComponentUuids = []; + const dfs = (node) => { + if (!node) { + return; + } + + for (const comp of node.components) { + if (comp.type === BTreeCompName) { + behaviorTreeComponentUuids.push(comp.value); + break; + } + } + for (const item of node.children) { + dfs(item); + } + }; + // 获取场景节点树 + const sceneNode = await Editor.Message.request("scene", "query-node-tree"); + + // 收集场景上所有BehaviorTree组件uuid + dfs(sceneNode); + + // 收集BehaviorTree组件上的json路径 + const rawUrls = await Promise.all( + behaviorTreeComponentUuids.map((uuid) => + Editor.Message.request("scene", "execute-component-method", { + uuid: uuid, + name: "getAssetUrl", + }) + ) + ); + + // 过滤空的并去重 + const urls = [...new Set(rawUrls.filter(Boolean))]; + // 根据url获取所有json文件信息 + const assets = await Promise.all(urls.map((url) => Editor.Message.request("asset-db", "query-asset-info", url))); + this.assets = assets.map(({ name, source, file }) => ({ name, url: source, file: file, content: "" })); + }, + async initSelection() { + // 找到当前选中的节点 + const node = await Editor.Message.request("scene", "query-node", Editor.Selection.getSelected("node")); + + // 未选中节点或者选中的是场景节点(场景节点不能添加组件) + if (!node?.__comps__) { + this.maskText = "请选中一个非场景根节点非空名称节点来开始行为树制作"; + return; + } + + // 找到BehaviorTree组件 + const index = node.__comps__.findIndex((v: any) => v.type === BTreeCompName); + if (index === -1) { + this.maskText = "要制作行为树,需要先为当前节点添加行为树组件"; + return; + } + + const comp = node.__comps__[index]; + // 调用BehaviorTree组件上的方法 + const url = await Editor.Message.request("scene", "execute-component-method", { + uuid: comp.value.uuid.value, + name: "getAssetUrl", + }); + + // JSON文件不存在 + if (!url) { + this.maskText = "当前行为树组件缺少JSON资源,点击BehaviorEditor组件按钮即可创建"; + return; + } + + this.maskText = ""; + + // 选中此文件,设置好currentAsset才能把组件nodes数据同步到JSON + this.handleSelectAsset(url); + }, + async handleSelectAsset(url) { + // 实时获取当前场景所有behaviorTree组件上的json文件 + await this.initAssets(); + const json = this.assets.find((e) => e.url === url); + if (!json) { + this.showWarn("JSON文件不存在"); + return; + } + + if (this.currentAsset?.url === json?.url) { + return; + } + + this.currentAsset = json; + + try { + const content = await fs.readJSONSync(json.file); + this.currentAsset.content = content; + this.render(); + } catch (e) { + if (e instanceof SyntaxError) { + // JSON文件语法异常的情况下,初始化内容 + this.showWarn("JSON文件内容初始化成功"); + const content = { nodes: [] }; + await fs.writeJSONSync(json.file, content); + this.currentAsset.content = content; + this.render(); + } else { + // 输出其他异常 + this.showWarn(e); + } + } + }, + handlePanelChange(value) { this.currentPanel = value; }, @@ -289,7 +461,7 @@ const component = Vue.extend({ this.render(); } }, - async handleEventNodeChange(lifeCycle, uuid = "") { + async handleEventNodeChange(lifeCycle, uuid = "", shouldSave = true) { if (!this.currentNode) { this.showWarn("当前节点不存在"); return; @@ -301,9 +473,13 @@ const component = Vue.extend({ } else { this.currentNode.event[lifeCycle].comp = ""; this.currentNode.event[lifeCycle].method = ""; - this.currentNode.event[lifeCycle].data = ""; + // 事件参数就不手动清空了 + // this.currentNode.event[lifeCycle].data = ""; + } + // 点击canvas某个节点的时候,会触发handleEventNodeChange获取场景节点数据,此时不保存数据 + if (shouldSave) { + await this.saveAsset(); } - await this.saveAsset(); }, async getNodeCompMethods(lifeCycle, uuid) { const [nodeInfo, compMethodInfo] = await Promise.all([ @@ -348,151 +524,18 @@ const component = Vue.extend({ this.currentNode.event[lifeCycle].data = data; await this.saveAsset(); }, - async selectCurrentAsset() { - // 找到点击组件的节点 - const node = await Editor.Message.request("scene", "query-node", Editor.Selection.getSelected("node")); - if (!node) { - this.showWarn(`未选中节点`); - return; - } - - // 找到BehaviorTree组件 - const index = node.__comps__.findIndex((v: any) => v.type === BTreeCompName); - if (index === -1) { - this.showWarn(`节点未挂载【${BTreeCompName}】组件`); - return; - } - - const comp = node.__comps__[index]; - // 调用BehaviorTree组件上的方法 - const url = await Editor.Message.request("scene", "execute-component-method", { - uuid: comp.value.uuid.value, - name: "getAssetUrl", - }); - - // JSON文件不存在 - if (!url) { - this.showWarn(`【${BTreeCompName}】组件未指定JSON文件`); - return; - } - - // 选中此文件,设置好currentAsset才能把组件nodes数据同步到JSON - this.handleSelectAsset(url); - }, - async handleSelectAsset(url) { - const json = this.assets.find((e) => e.url === url); - if (!json) { - this.showWarn("JSON文件不存在"); - return; - } - this.currentAsset = json; - - try { - const content = await fs.readJSONSync(json.file); - this.currentAsset.content = content; - this.render(); - } catch (e) { - if (e instanceof SyntaxError) { - // JSON文件语法异常的情况下,初始化内容 - this.showWarn("JSON文件内容初始化成功"); - const content = { nodes: [] }; - await fs.writeJSONSync(json.file, content); - this.currentAsset.content = content; - this.render(); - } else { - // 输出其他异常 - this.showWarn(e); - } - } - }, - // 全部所有behaviorTree组件上使用到的json文件 - async getAssets() { - const behaviorTreeComponentUuids = []; - const dfs = (node) => { - if (!node) { - return; - } - - for (const comp of node.components) { - if (comp.type === BTreeCompName) { - behaviorTreeComponentUuids.push(comp.value); - break; - } - } - for (const item of node.children) { - dfs(item); - } - }; - // 获取场景节点树 - const sceneNode = await Editor.Message.request("scene", "query-node-tree"); - - // 收集场景上所有BehaviorTree组件uuid - dfs(sceneNode); - - // 收集BehaviorTree组件上的json路径 - const rawUrls = await Promise.all( - behaviorTreeComponentUuids.map((uuid) => - Editor.Message.request("scene", "execute-component-method", { - uuid: uuid, - name: "getAssetUrl", - }) - ) - ); - - // 过滤空的并去重 - const urls = [...new Set(rawUrls.filter(Boolean))]; - // 根据url获取所有json文件信息 - const assets = await Promise.all(urls.map((url) => Editor.Message.request("asset-db", "query-asset-info", url))); - - this.assets = assets.map(({ name, source, file }) => ({ name, url: source, file: file, content: "" })); - }, async saveAsset() { - const url = this.currentAsset?.url; - if (!url) { + const file = this.currentAsset?.file; + if (!file) { this.showWarn("数据存储失败,未指定JSON"); return; } - const content = JSON.stringify(this.currentAsset.content, null, 2); + const content = this.currentAsset.content; + // 修改json文件 - await Editor.Message.request("asset-db", "save-asset", url, content); + fs.writeJSONSync(file, content); }, - initCanvas() { - // 初始化舞台 - stage = new Konva.Stage({ - container: this.$refs.tree, - width: CANVAS_WIDTH, - height: CANVAS_HEIGHT, - }); - // 基础层,目前只有一层,每层都会生成一个canvas - baseLayer = new Konva.Layer(); - // 背景组 - bgGroup = this.generateBg(); - baseLayer.add(bgGroup); - - // 静态箭头层 - staticArrowGroup = new Konva.Group(); - baseLayer.add(staticArrowGroup); - - // 动态箭头层(用户手动拉出来的箭头) - dynamicArrowGroup = this.generateDynamicArrowGroup(); - baseLayer.add(dynamicArrowGroup); - - // Root组 - rootGroup = this.generateRoot(); - baseLayer.add(rootGroup); - - // 节点层 - nodeGroup = new Konva.Group(); - baseLayer.add(nodeGroup); - - // 框选层 - selectionGroup = this.generatesSelectionGroup(); - baseLayer.add(selectionGroup); - - // 层添加到舞台 - stage.add(baseLayer); - }, /*** * 渲染画布 * renderNode:拖拽的时候不渲染节点 @@ -616,7 +659,7 @@ const component = Vue.extend({ type, abortType: this.AbortType.None, x: (CANVAS_WIDTH - BOX_WIDTH) / 2, - y: CANVAS_HEIGHT / 4, + y: ROOT_Y + 100, isRoot: false, children: [], event: { @@ -895,7 +938,7 @@ const component = Vue.extend({ if (e.evt.buttons === 4) { const movementX = e.evt.movementX; const movementY = e.evt.movementY; - this.$refs.left.scrollBy(-movementX, -movementY); + this.$refs.scroll.scrollBy(-movementX, -movementY); return; } if (!selection.visible()) { @@ -1063,11 +1106,11 @@ const component = Vue.extend({ selectedBoxes = []; this.currentNode = node; this.handlePanelChange(1); - this.render(); // 获取节点事件相关信息 - this.handleEventNodeChange(this.onStart, this.currentNode.event[this.onStart].node); - this.handleEventNodeChange(this.onUpdate, this.currentNode.event[this.onUpdate].node); - this.handleEventNodeChange(this.onEnd, this.currentNode.event[this.onEnd].node); + this.handleEventNodeChange(this.onStart, this.currentNode.event[this.onStart].node, false); + this.handleEventNodeChange(this.onUpdate, this.currentNode.event[this.onUpdate].node, false); + this.handleEventNodeChange(this.onEnd, this.currentNode.event[this.onEnd].node, false); + this.render(); }); return wrapper; @@ -1349,6 +1392,13 @@ const component = Vue.extend({ return BOX_HEIGHT; }, + isComposite(node) { + if (!node) { + return false; + } + + return CompositeNodes.includes(node.type); + }, showWarn(text) { let time = getTime(); const content = `${time}:${text}`; @@ -1358,6 +1408,10 @@ const component = Vue.extend({ this.logs.push({ id: uuid(), content }); console.warn(content); }, + // 视角回到root节点 + backRoot() { + this.$refs?.scroll?.scroll((CANVAS_WIDTH - this.$refs.scroll.getBoundingClientRect().width) / 2, ROOT_Y - 50); + }, }, }); const panelDataMap = new WeakMap() as WeakMap>; @@ -1383,7 +1437,14 @@ module.exports = Editor.Panel.define({ data() { return {}; }, - methods: {}, + methods: { + initSelection() { + const vm = panelDataMap.get(this); + if (vm) { + vm.initSelection(); + } + }, + }, ready() { if (this.$.app) { const vm = new component(); diff --git a/src/panels/static/style/default/index.css b/src/panels/static/style/default/index.css index 4c6ec18..aeddbaf 100644 --- a/src/panels/static/style/default/index.css +++ b/src/panels/static/style/default/index.css @@ -1,7 +1,8 @@ .wrapper { display: flex; background-color: #2b2b2b; - max-height: calc(100vh - 26px); + width: 100%; + height: 100%; font-size: 12px; color: #cccccc; font-family: BlinkMacSystemFont, "PingFang SC", system-ui, -apple-system, Helvetica Neue, Helvetica, sans-serif; @@ -15,12 +16,24 @@ ui-prop[no-label] { padding: 0px 4px 0 4px; } +.node-type { + padding-right: 8px; +} + +.life-cycle-input { + margin-top: 4px; +} + .right { - width: 280px; - border-left: 1px solid #050505; + width: 260px; + border-left: 2px solid #050505; flex-shrink: 0; } +.right > div { + padding-right: 2px; +} + .node-panel { display: flex; flex-direction: column; @@ -29,25 +42,41 @@ ui-prop[no-label] { .left { position: relative; - overflow: scroll; + width: calc(100% - 260px); + display: flex; +} + +.scroll { + overflow: auto; } .fix-left { - position: fixed; - left: 13px; - top: 34px; + position: absolute; + left: 14px; + top: 14px; z-index: 1; display: flex; } .fix-right { - position: fixed; - right: 300px; - top: 34px; + position: absolute; + right: 14px; + top: 14px; z-index: 1; display: flex; } +.fix-bottom { + position: absolute; + left: 14px; + bottom: 24px; + z-index: 1; +} + +.fix-bottom ui-button { + padding: 0 12px; +} + .asset { } @@ -55,7 +84,7 @@ ui-prop[no-label] { color: #e8b116; min-width: 200px; max-width: 500px; - font-weight: bold; + /* font-weight: bold; */ } .log div { @@ -64,6 +93,10 @@ ui-prop[no-label] { white-space: nowrap; } +.log div:first-child { + padding-top: 8px; +} + .flex { display: flex; } @@ -102,10 +135,22 @@ ui-prop[no-label] { margin-top: 4px; } +.mt-12 { + margin-top: 12px; +} + +.mr-4 { + margin-right: 4px; +} + .mr-6 { margin-right: 6px; } +.mr-8 { + margin-right: 8px; +} + .grow { flex-grow: 1; } @@ -113,3 +158,16 @@ ui-prop[no-label] { .flex-1 { flex: 1 1 0%; } + +.mask { + position: absolute; + left: 0; + right: 0; + bottom: 0; + top: 0; + z-index: 999; + background-color: rgba(2, 2, 2, 0.6); + display: flex; + justify-content: center; + align-items: center; +} diff --git a/src/panels/static/template/default/index.html b/src/panels/static/template/default/index.html index 2c988fc..a124a05 100644 --- a/src/panels/static/template/default/index.html +++ b/src/panels/static/template/default/index.html @@ -1,3 +1,3 @@ -
+
diff --git a/src/panels/static/template/vue/app.html b/src/panels/static/template/vue/app.html index b54f0c2..f4d2378 100644 --- a/src/panels/static/template/vue/app.html +++ b/src/panels/static/template/vue/app.html @@ -1,5 +1,9 @@
-
+
{{maskText}}
+
+
+
+
@@ -14,7 +18,9 @@
-
+
+ Root +
@@ -24,7 +30,7 @@
- {{ item.type }} @@ -53,6 +59,7 @@ Abort Type @@ -93,9 +100,10 @@
- + @@ -107,7 +115,7 @@ {{onUpdate | toUpperCase}} -
+
- + @@ -148,7 +157,7 @@ {{onEnd | toUpperCase}} -
+
- + @@ -186,7 +196,7 @@
-
请选择节点
+
Please select a node